Initial commit

germanium cutoff
This commit is contained in:
Timothy J. Aveni
2026-05-24 15:34:50 -07:00
commit 2cdd578848
36 changed files with 7197 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
+4
View File
@@ -0,0 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
/.pnp.* binary linguist-generated
+19
View File
@@ -0,0 +1,19 @@
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Whether you use PnP or not, the node_modules folder is often used to store
# build artifacts that should be gitignored
node_modules
# Swap the comments on the following lines if you wish to use zero-installs
# In that case, don't forget to run `yarn config set enableGlobalCache false`!
# Documentation here: https://yarnpkg.com/features/caching#zero-installs
#!.yarn/cache
.pnp.*
*.swp
+122
View File
@@ -0,0 +1,122 @@
import z, { type ZodType } from "zod";
import { type JsonValue } from "./browserSurfaceShared.js";
import { type CreatePackageOptions, type EventSchemaMap, type PackageFunctionSchema, type PackageFunctions, type PackageContext, type PackageSchema, type SchemaAnnotation, type ValueSchemaMap } from "./index.js";
export { jsonValueSchema, type JsonValue } from "./browserSurfaceShared.js";
export { createEmbeddedSurfaceRef, createEmbeddedSurfaceRefSchema, embeddedSurfaceRefSchema, getEmbeddedSurfaceId, getEmbeddedSurfacePackageName, type EmbeddedSurfaceRef, } from "./browserSurfaceShared.js";
export type { SubscriptionHandle } from "./index.js";
export declare const stateContextResultSchema: z.ZodObject<{
stateContextId: z.ZodString;
}, z.z.core.$strip>;
export declare const createBrowserSurfaceAnnotation: ({ surfaceId, namespaceProp, surfaces, capabilities, aspectRatioHint, propsFunction, }: {
surfaceId: string;
namespaceProp?: string;
surfaces?: string[];
capabilities?: string[];
aspectRatioHint?: string;
propsFunction?: string;
}) => SchemaAnnotation;
export declare const createBrowserSurfacePropsUpdateFunctionSchema: <PropsSchema extends z.ZodTypeAny>({ propsSchema, namespaceProp, description, inputDescription, outputDescription, }: {
propsSchema: PropsSchema;
namespaceProp?: string;
description: string;
inputDescription?: string;
outputDescription?: string;
}) => PackageFunctionSchema;
export declare const createBrowserSurfacePackageSchema: <PropsSchema extends z.ZodTypeAny, ExtraFunctions extends Record<string, PackageFunctionSchema> = Record<never, never>, Events extends EventSchemaMap = Record<never, never>, Values extends ValueSchemaMap = Record<never, never>>({ description, majorVersion, surfaceId, propsSchema, namespaceProp, surfaces, capabilities, aspectRatioHint, propsFunctionDescription, propsInputDescription, propsOutputDescription, annotations, functions, events, values, }: {
description: string;
majorVersion?: number;
surfaceId: string;
propsSchema: PropsSchema;
namespaceProp?: string;
surfaces?: string[];
capabilities?: string[];
aspectRatioHint?: string;
propsFunctionDescription: string;
propsInputDescription?: string;
propsOutputDescription?: string;
annotations?: SchemaAnnotation[];
functions?: ExtraFunctions;
events?: Events;
values?: Values;
}) => PackageSchema<{
propsUpdate: PackageFunctionSchema;
} & ExtraFunctions, Events, Values>;
type EventSink<T> = {
emit: (data: T) => Promise<void>;
};
export type EventHub<Events extends Record<string, unknown>> = {
subscribe: <K extends keyof Events>(eventName: K, sink: EventSink<Events[K]>) => () => void;
emit: <K extends keyof Events>(eventName: K, payload: Events[K], onError?: (error: unknown, eventName: K) => void) => void;
};
export declare const createEventHub: <Events extends Record<string, unknown>>() => EventHub<Events>;
type ProducerHandlers = {
onHostAction?: (hostSessionId: string, action: JsonValue) => void | Promise<void>;
onHostDetached?: (hostSessionId: string) => void | Promise<void>;
onHostError?: (hostSessionId: string, error: {
message: string;
stack?: string;
}) => void | Promise<void>;
};
export declare class BrowserSurfaceRelayProducerClient {
#private;
readonly relayUrl: string;
constructor(relayUrl?: string);
connect(): Promise<void>;
close(): void;
attach(stateContextId: string, surfaceId: string, handlers?: ProducerHandlers): Promise<void>;
publishProps(stateContextId: string, surfaceId: string, props: unknown): Promise<void>;
publishMessage(stateContextId: string, surfaceId: string, payload: unknown): Promise<void>;
detach(stateContextId: string, surfaceId: string): Promise<void>;
}
type DependencyClient = ReturnType<PackageContext["usePackage"]>;
type RelaySchema = Parameters<PackageContext["usePackage"]>[0];
type BrowserSurfaceInstanceInternal<State> = {
ctx: PackageContext;
stateContextId: string;
relay: DependencyClient;
producer: BrowserSurfaceRelayProducerClient;
state: State;
publishQueue: Promise<void>;
publish: () => void;
publishMessage: (payload: unknown) => Promise<void>;
};
export type BrowserSurfaceInstance<State> = Omit<BrowserSurfaceInstanceInternal<State>, "publishQueue">;
export type BrowserSurfaceHostError = {
message: string;
stack?: string;
};
export type BrowserSurfaceControllerOptions<State, Props, Action> = {
relaySchema: RelaySchema;
surfaceId: string;
bundleDir: string;
entryPoint: string;
propsSchema: ZodType<Props>;
actionSchema: ZodType<Action>;
logPrefix: string;
createState: (instance: BrowserSurfaceInstance<State>) => State | Promise<State>;
buildProps: (instance: BrowserSurfaceInstance<State>) => unknown;
applyProps: (instance: BrowserSurfaceInstance<State>, props: Props) => void | Promise<void>;
onHostAction?: (instance: BrowserSurfaceInstance<State>, hostSessionId: string, action: Action) => void | Promise<void>;
onHostDetached?: (instance: BrowserSurfaceInstance<State>, hostSessionId: string) => void | Promise<void>;
onHostError?: (instance: BrowserSurfaceInstance<State>, hostSessionId: string, error: BrowserSurfaceHostError) => void | Promise<void>;
onAfterCreate?: (instance: BrowserSurfaceInstance<State>) => void | Promise<void>;
onBeforeDestroy?: (instance: BrowserSurfaceInstance<State>) => void | Promise<void>;
};
type BrowserSurfaceController<Context extends PackageContext = PackageContext> = {
onContextOpen: (ctx: Context) => void | Promise<void>;
onContextClose: (ctx: Context) => void | Promise<void>;
onDestroy: () => void | Promise<void>;
propsUpdate: (ctx: Context, nextProps: unknown) => Promise<string>;
};
export declare const createBrowserSurfacePackage: <Schema extends PackageSchema, Context extends PackageContext = PackageContext>({ schema, surface, functions, events, values, onCreate, onContextOpen, onContextClose, onDestroy, }: Omit<CreatePackageOptions<Schema, Context>, "functions"> & {
surface: BrowserSurfaceController<Context>;
functions?: Omit<PackageFunctions<Schema["functions"], Context>, "propsUpdate">;
}) => {};
export declare const createBrowserSurfaceController: <State, Props, Action>({ relaySchema, surfaceId, bundleDir, entryPoint, propsSchema, actionSchema, logPrefix, createState, buildProps, applyProps, onHostAction, onHostDetached, onHostError, onAfterCreate, onBeforeDestroy, }: BrowserSurfaceControllerOptions<State, Props, Action>) => {
getOrCreate: (ctx: PackageContext) => Promise<BrowserSurfaceInstance<State>>;
onContextOpen: (ctx: PackageContext) => Promise<void>;
onContextClose: (ctx: PackageContext) => Promise<void>;
onDestroy: () => Promise<void>;
propsUpdate: (ctx: PackageContext, nextProps: unknown) => Promise<string>;
};
//# sourceMappingURL=browserSurface.d.ts.map
File diff suppressed because one or more lines are too long
+403
View File
@@ -0,0 +1,403 @@
import WebSocket from "ws";
import z, {} from "zod";
import { createEmbeddedSurfaceRef, createEmbeddedSurfaceRefSchema, embeddedSurfaceRefSchema, getEmbeddedSurfaceId, getEmbeddedSurfacePackageName, jsonValueSchema, } from "./browserSurfaceShared.js";
import { createPackage, reportPackageRuntimeError, } from "./index.js";
export { jsonValueSchema } from "./browserSurfaceShared.js";
export { createEmbeddedSurfaceRef, createEmbeddedSurfaceRefSchema, embeddedSurfaceRefSchema, getEmbeddedSurfaceId, getEmbeddedSurfacePackageName, } from "./browserSurfaceShared.js";
export const stateContextResultSchema = z.object({
stateContextId: z.string().min(1),
});
export const createBrowserSurfaceAnnotation = ({ surfaceId, namespaceProp = "quixosKey", surfaces = ["desktop"], capabilities = ["mouse", "keyboard"], aspectRatioHint = "16:10", propsFunction = "propsUpdate", }) => ({
type: "quixos.ui.surface/v1",
surfaceId,
runtime: "browser-module",
transport: "relay",
propsFunction,
namespaceProp,
surfaces,
capabilities,
aspectRatioHint,
});
export const createBrowserSurfacePropsUpdateFunctionSchema = ({ propsSchema, namespaceProp = "quixosKey", description, inputDescription, outputDescription, }) => ({
description,
annotations: [
{
type: "quixos.ui.browser-surface-props/v1",
namespaceProp,
semantics: "replace",
transport: "relay",
},
],
inputSchema: (inputDescription
? z.object({ props: propsSchema }).describe(inputDescription)
: z.object({ props: propsSchema })),
outputSchema: (outputDescription
? stateContextResultSchema.describe(outputDescription)
: stateContextResultSchema),
});
export const createBrowserSurfacePackageSchema = ({ description, majorVersion = 1, surfaceId, propsSchema, namespaceProp = "quixosKey", surfaces = ["desktop"], capabilities = ["mouse", "keyboard"], aspectRatioHint = "16:10", propsFunctionDescription, propsInputDescription, propsOutputDescription, annotations = [], functions, events, values, }) => ({
schemaVersion: 1,
majorVersion,
description,
annotations: [
createBrowserSurfaceAnnotation({
surfaceId,
namespaceProp,
surfaces,
capabilities,
aspectRatioHint,
propsFunction: "propsUpdate",
}),
...annotations,
],
functions: {
...(functions ?? {}),
propsUpdate: createBrowserSurfacePropsUpdateFunctionSchema({
propsSchema,
namespaceProp,
description: propsFunctionDescription,
inputDescription: propsInputDescription,
outputDescription: propsOutputDescription,
}),
},
events: (events ?? {}),
values: (values ?? {}),
});
export const createEventHub = () => {
const sinks = new Map();
return {
subscribe: (eventName, sink) => {
let eventSinks = sinks.get(eventName);
if (!eventSinks) {
eventSinks = new Set();
sinks.set(eventName, eventSinks);
}
eventSinks.add(sink);
return () => {
eventSinks?.delete(sink);
};
},
emit: (eventName, payload, onError) => {
const eventSinks = sinks.get(eventName);
if (!eventSinks) {
return;
}
for (const sink of eventSinks) {
void sink
.emit(payload)
.catch((error) => onError?.(error, eventName));
}
},
};
};
const DEFAULT_SURFACE_RELAY_URL = "ws://127.0.0.1:6247";
const producerMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("browser-surface-host-action"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
hostSessionId: z.string().min(1),
action: jsonValueSchema,
}),
z.object({
type: z.literal("browser-surface-host-detached"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
hostSessionId: z.string().min(1),
}),
z.object({
type: z.literal("browser-surface-host-error"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
hostSessionId: z.string().min(1),
message: z.string().min(1),
stack: z.string().min(1).optional(),
}),
z.object({
type: z.literal("error"),
message: z.string(),
}),
]);
const surfaceKey = (stateContextId, surfaceId) => JSON.stringify([stateContextId, surfaceId]);
const getErrorMessage = (error) => error instanceof Error ? error.message : String(error);
const parseProducerMessage = (value) => {
const parsed = producerMessageSchema.safeParse(value);
return parsed.success ? parsed.data : null;
};
export class BrowserSurfaceRelayProducerClient {
relayUrl;
#socket = null;
#ready = null;
#handlers = new Map();
constructor(relayUrl = DEFAULT_SURFACE_RELAY_URL) {
this.relayUrl = relayUrl;
}
async connect() {
if (this.#ready) {
return await this.#ready;
}
this.#ready = new Promise((resolve, reject) => {
const socket = new WebSocket(this.relayUrl);
this.#socket = socket;
let opened = false;
const fail = (error) => {
const message = new Error(getErrorMessage(error));
this.#socket = null;
this.#ready = null;
if (!opened) {
reject(message);
return;
}
console.error(`[browser-surface] relay websocket disconnected: ${message.message}`);
};
socket.on("open", () => {
opened = true;
resolve();
});
socket.on("message", (rawData) => {
let parsedJson;
try {
parsedJson = JSON.parse(String(rawData));
}
catch {
return;
}
const message = parseProducerMessage(parsedJson);
if (!message) {
return;
}
if (message.type === "error") {
console.error(`[browser-surface] relay error: ${message.message}`);
return;
}
const handlers = this.#handlers.get(surfaceKey(message.stateContextId, message.surfaceId));
if (!handlers) {
return;
}
if (message.type === "browser-surface-host-action") {
void handlers.onHostAction?.(message.hostSessionId, message.action);
return;
}
if (message.type === "browser-surface-host-error") {
void handlers.onHostError?.(message.hostSessionId, {
message: message.message,
stack: message.stack,
});
return;
}
void handlers.onHostDetached?.(message.hostSessionId);
});
socket.on("error", () => {
fail(new Error("Surface relay websocket error"));
});
socket.on("close", () => {
fail(new Error("Surface relay websocket closed"));
});
});
return await this.#ready;
}
close() {
this.#socket?.close();
this.#socket = null;
this.#ready = null;
this.#handlers.clear();
}
async #send(message) {
await this.connect();
const socket = this.#socket;
if (!socket) {
throw new Error("Surface relay websocket is not connected");
}
socket.send(JSON.stringify(message));
}
async attach(stateContextId, surfaceId, handlers = {}) {
this.#handlers.set(surfaceKey(stateContextId, surfaceId), handlers);
await this.#send({
type: "browser-surface-producer-attach",
stateContextId,
surfaceId,
});
}
async publishProps(stateContextId, surfaceId, props) {
await this.#send({
type: "browser-surface-props",
stateContextId,
surfaceId,
props,
});
}
async publishMessage(stateContextId, surfaceId, payload) {
await this.#send({
type: "browser-surface-message",
stateContextId,
surfaceId,
payload,
});
}
async detach(stateContextId, surfaceId) {
this.#handlers.delete(surfaceKey(stateContextId, surfaceId));
try {
await this.#send({
type: "browser-surface-producer-detach",
stateContextId,
surfaceId,
});
}
catch {
// ignore shutdown races
}
}
}
export const createBrowserSurfacePackage = ({ schema, surface, functions, events, values, onCreate, onContextOpen, onContextClose, onDestroy, }) => createPackage({
schema,
onCreate,
onContextOpen: async (ctx) => {
await surface.onContextOpen(ctx);
await onContextOpen?.(ctx);
},
onContextClose: async (ctx) => {
try {
await onContextClose?.(ctx);
}
finally {
await surface.onContextClose(ctx);
}
},
onDestroy: async () => {
try {
await onDestroy?.();
}
finally {
await surface.onDestroy();
}
},
functions: {
...(functions ?? {}),
propsUpdate: async (ctx, params) => stateContextResultSchema.parse({
stateContextId: await surface.propsUpdate(ctx, params.props),
}),
},
events: events,
values: values,
});
const toPublicInstance = (instance) => instance;
export const createBrowserSurfaceController = ({ relaySchema, surfaceId, bundleDir, entryPoint, propsSchema, actionSchema, logPrefix, createState, buildProps, applyProps, onHostAction, onHostDetached, onHostError, onAfterCreate, onBeforeDestroy, }) => {
const renderStates = new Map();
const pendingRenderStates = new Map();
const queuePublish = (instance) => {
instance.publishQueue = instance.publishQueue
.then(async () => {
await instance.producer.publishProps(instance.stateContextId, surfaceId, buildProps(toPublicInstance(instance)));
})
.catch((error) => {
console.error(`${logPrefix} failed to publish surface props for ${instance.stateContextId}`, error);
});
};
const createRenderState = async (ctx) => {
const relay = ctx.usePackage(relaySchema);
const registration = (await relay.functions.registerBrowserSurface({
stateContextId: ctx.stateContext,
surfaceId,
bundleDir,
entryPoint,
}));
const producer = new BrowserSurfaceRelayProducerClient(registration.relayUrl);
const instance = {
ctx,
stateContextId: ctx.stateContext,
relay,
producer,
state: undefined,
publishQueue: Promise.resolve(),
publish: () => {
queuePublish(instance);
},
publishMessage: async (payload) => {
await producer.publishMessage(ctx.stateContext, surfaceId, payload);
},
};
instance.state = await createState(toPublicInstance(instance));
renderStates.set(ctx.stateContext, instance);
await producer.attach(ctx.stateContext, surfaceId, {
onHostAction: async (hostSessionId, action) => {
if (!onHostAction) {
return;
}
const parsed = actionSchema.parse(action);
await onHostAction(toPublicInstance(instance), hostSessionId, parsed);
},
onHostDetached: async (hostSessionId) => {
await onHostDetached?.(toPublicInstance(instance), hostSessionId);
},
onHostError: async (hostSessionId, error) => {
await reportPackageRuntimeError({
phase: "browser-surface",
error: new Error(error.message),
stateContextId: instance.stateContextId,
surfaceId,
hostSessionId,
stack: error.stack,
});
await onHostError?.(toPublicInstance(instance), hostSessionId, error);
},
});
await onAfterCreate?.(toPublicInstance(instance));
instance.publish();
return instance;
};
const getOrCreate = async (ctx) => {
const existing = renderStates.get(ctx.stateContext);
if (existing) {
return existing;
}
const pending = pendingRenderStates.get(ctx.stateContext);
if (pending) {
return await pending;
}
const creation = createRenderState(ctx);
pendingRenderStates.set(ctx.stateContext, creation);
try {
return await creation;
}
finally {
pendingRenderStates.delete(ctx.stateContext);
}
};
const destroyState = async (stateContextId) => {
const instance = renderStates.get(stateContextId);
if (!instance) {
return;
}
renderStates.delete(stateContextId);
pendingRenderStates.delete(stateContextId);
await instance.publishQueue.catch(() => { });
await onBeforeDestroy?.(toPublicInstance(instance));
await instance.producer.detach(stateContextId, surfaceId);
instance.producer.close();
await instance.relay.functions.unregisterBrowserSurface({ stateContextId, surfaceId });
};
return {
getOrCreate: async (ctx) => toPublicInstance(await getOrCreate(ctx)),
onContextOpen: async (ctx) => {
await getOrCreate(ctx);
},
onContextClose: async (ctx) => {
await destroyState(ctx.stateContext);
},
onDestroy: async () => {
const activeStateContexts = [...renderStates.keys()];
pendingRenderStates.clear();
for (const stateContextId of activeStateContexts) {
await destroyState(stateContextId);
}
},
propsUpdate: async (ctx, nextProps) => {
const parsedProps = propsSchema.parse(nextProps);
const instance = await getOrCreate(ctx);
await applyProps(toPublicInstance(instance), parsedProps);
instance.publish();
return ctx.stateContext;
},
};
};
//# sourceMappingURL=browserSurface.js.map
File diff suppressed because one or more lines are too long
+33
View File
@@ -0,0 +1,33 @@
import { type CSSProperties, type ReactNode } from "react";
import type { PackageSchema } from "./index.js";
import { type EmbeddedSurfaceRef } from "./browserSurfaceShared.js";
type BrowserSurfaceSchemaLike = PackageSchema & {
__quixos?: {
name?: string | null;
flakeRef?: string | null;
};
};
type EmbeddedSurfaceHostProps = {
surface: EmbeddedSurfaceRef;
relayUrl?: string;
className?: string;
style?: CSSProperties;
fallback?: ReactNode;
onError?: (error: Error) => void;
};
export type EmbeddedSurfaceComponentProps<SurfaceId extends string = string> = {
stateContextId: string;
surfaceId?: SurfaceId;
relayUrl?: string;
className?: string;
style?: CSSProperties;
fallback?: ReactNode;
onError?: (error: Error) => void;
};
export declare const EmbeddedSurface: ({ surface, relayUrl, className, style, fallback, onError, }: EmbeddedSurfaceHostProps) => import("react/jsx-runtime").JSX.Element;
export declare const createEmbeddedSurfaceComponent: <Target extends string | BrowserSurfaceSchemaLike, SurfaceId extends string = string>(target: Target, options?: {
surfaceId?: SurfaceId;
relayUrl?: string;
}) => ({ stateContextId, surfaceId, relayUrl, className, style, fallback, onError, }: EmbeddedSurfaceComponentProps<SurfaceId>) => import("react/jsx-runtime").JSX.Element;
export {};
//# sourceMappingURL=browserSurfaceEmbedded.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browserSurfaceEmbedded.d.ts","sourceRoot":"","sources":["../../src/browserSurfaceEmbedded.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAMnC,KAAK,wBAAwB,GAAG,aAAa,GAAG;IAC9C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;CACH,CAAC;AA6HF,KAAK,wBAAwB,GAAG;IAC9B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,6BAA6B,CAAC,SAAS,SAAS,MAAM,GAAG,MAAM,IAAI;IAC7E,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAmUF,eAAO,MAAM,eAAe,GAAI,6DAO7B,wBAAwB,4CA6L1B,CAAC;AAEF,eAAO,MAAM,8BAA8B,GACzC,MAAM,SAAS,MAAM,GAAG,wBAAwB,EAChD,SAAS,SAAS,MAAM,GAAG,MAAM,EAEjC,QAAQ,MAAM,EACd,UAAU;IACR,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,MAOO,+EAQH,6BAA6B,CAAC,SAAS,CAAC,4CAa9C,CAAC"}
+444
View File
@@ -0,0 +1,444 @@
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
import { useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
import { createEmbeddedSurfaceRef, getEmbeddedSurfaceId, } from "./browserSurfaceShared.js";
import { jsonValueSchema, } from "./browserSurfaceShared.js";
import z from "zod";
const LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
const RELAY_LOCAL_PORT = "6247";
const RELAY_PROXY_PATH = "/relay/";
const relayServerMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("browser-surface-bootstrap"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
bundleUrl: z.string().url(),
initialProps: jsonValueSchema,
}),
z.object({
type: z.literal("browser-surface-props"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
props: jsonValueSchema,
}),
z.object({
type: z.literal("browser-surface-message"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
payload: jsonValueSchema,
}),
z.object({
type: z.literal("browser-surface-closed"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
}),
z.object({
type: z.literal("error"),
message: z.string(),
}),
]);
const isLocalHostname = (hostname) => LOCAL_HOSTNAMES.has(hostname);
const urlForCurrentHostAndPort = (locationLike = window.location, port, { ws = false } = {}) => {
const url = new URL(locationLike.href);
url.protocol = ws
? url.protocol === "https:"
? "wss:"
: "ws:"
: url.protocol === "https:"
? "https:"
: "http:";
url.port = port;
url.pathname = "/";
url.search = "";
url.hash = "";
return url.toString();
};
const urlForCurrentOriginAndPath = (locationLike = window.location, path, { ws = false } = {}) => {
const url = new URL(locationLike.href);
url.protocol = ws
? url.protocol === "https:"
? "wss:"
: "ws:"
: url.protocol === "https:"
? "https:"
: "http:";
url.port = "";
url.pathname = path;
url.search = "";
url.hash = "";
return url.toString();
};
const defaultRelayUrlForBrowser = (locationLike = window.location) => {
if (isLocalHostname(locationLike.hostname)) {
return urlForCurrentHostAndPort(locationLike, RELAY_LOCAL_PORT, {
ws: true,
});
}
return urlForCurrentOriginAndPath(locationLike, RELAY_PROXY_PATH, {
ws: true,
});
};
const getErrorMessage = (error) => error instanceof Error ? error.message : String(error);
const toError = (error) => error instanceof Error ? error : new Error(String(error));
const getErrorDetails = (error) => {
if (error instanceof Error) {
return {
message: error.message || "Unknown error",
stack: error.stack,
};
}
return {
message: String(error),
stack: undefined,
};
};
const parseServerMessage = (value) => {
const parsed = relayServerMessageSchema.safeParse(value);
return parsed.success ? parsed.data : null;
};
const normalizeSurfaceModule = (module) => {
if (typeof module === "object" &&
module !== null &&
typeof module.mount === "function") {
return module;
}
throw new Error("Surface bundle does not export a mount(...) function");
};
const attachmentKey = (stateContextId, surfaceId, hostSessionId) => JSON.stringify([stateContextId, surfaceId, hostSessionId]);
const sharedBrowserSurfaceRelayClients = new Map();
const createBrowserSurfaceHostSessionId = () => crypto.randomUUID();
class BrowserSurfaceRelayClient {
relayUrl;
#socket = null;
#ready = null;
#attachments = new Map();
constructor(relayUrl) {
this.relayUrl = relayUrl;
}
async connect() {
if (this.#ready) {
return await this.#ready;
}
this.#ready = new Promise((resolve, reject) => {
const socket = new WebSocket(this.relayUrl);
this.#socket = socket;
let opened = false;
const fail = (error) => {
const message = toError(error);
this.#socket = null;
this.#ready = null;
if (!opened) {
reject(message);
}
};
socket.addEventListener("open", () => {
opened = true;
resolve();
});
socket.addEventListener("message", (event) => {
let parsedJson;
try {
parsedJson = JSON.parse(String(event.data));
}
catch {
return;
}
const message = parseServerMessage(parsedJson);
if (!message) {
return;
}
if (message.type === "error") {
console.error(`[browser-surface-relay] ${message.message}`);
return;
}
const attachment = this.#findAttachment(message.stateContextId, message.surfaceId);
if (!attachment) {
return;
}
void attachment.handler(message);
});
socket.addEventListener("error", () => {
fail(new Error("Browser surface relay socket error"));
});
socket.addEventListener("close", () => {
fail(new Error("Browser surface relay socket closed"));
});
});
return await this.#ready;
}
close() {
this.#socket?.close();
this.#socket = null;
this.#ready = null;
this.#attachments.clear();
}
async #send(message) {
await this.connect();
const socket = this.#socket;
if (!socket) {
throw new Error("Browser surface relay socket is not connected");
}
socket.send(JSON.stringify(message));
}
#findAttachment(stateContextId, surfaceId) {
for (const attachment of this.#attachments.values()) {
if (attachment.stateContextId === stateContextId &&
attachment.surfaceId === surfaceId) {
return attachment;
}
}
return null;
}
async attach(stateContextId, surfaceId, hostSessionId, handler) {
const key = attachmentKey(stateContextId, surfaceId, hostSessionId);
const existing = this.#findAttachment(stateContextId, surfaceId);
if (existing) {
for (const [existingKey, attachment] of this.#attachments.entries()) {
if (attachment === existing) {
this.#attachments.delete(existingKey);
break;
}
}
try {
await this.#send({
type: "browser-surface-host-detach",
stateContextId,
surfaceId,
hostSessionId: existing.hostSessionId,
});
}
catch {
// Ignore replacement races.
}
}
this.#attachments.set(key, {
stateContextId,
surfaceId,
hostSessionId,
handler,
});
await this.#send({
type: "browser-surface-host-attach",
stateContextId,
surfaceId,
hostSessionId,
});
return async () => {
this.#attachments.delete(key);
try {
await this.#send({
type: "browser-surface-host-detach",
stateContextId,
surfaceId,
hostSessionId,
});
}
catch {
// Ignore detach races during teardown.
}
};
}
async sendAction(stateContextId, surfaceId, hostSessionId, action) {
await this.#send({
type: "browser-surface-host-action",
stateContextId,
surfaceId,
hostSessionId,
action,
});
}
async sendError(stateContextId, surfaceId, hostSessionId, error) {
await this.#send({
type: "browser-surface-host-error",
stateContextId,
surfaceId,
hostSessionId,
message: error.message,
...(error.stack ? { stack: error.stack } : {}),
});
}
}
const acquireSharedBrowserSurfaceRelayClient = (relayUrl) => {
const existing = sharedBrowserSurfaceRelayClients.get(relayUrl);
if (existing) {
existing.refCount += 1;
return existing.client;
}
const client = new BrowserSurfaceRelayClient(relayUrl);
sharedBrowserSurfaceRelayClients.set(relayUrl, {
client,
refCount: 1,
});
return client;
};
const releaseSharedBrowserSurfaceRelayClient = (relayUrl) => {
const entry = sharedBrowserSurfaceRelayClients.get(relayUrl);
if (!entry) {
return;
}
entry.refCount -= 1;
if (entry.refCount > 0) {
return;
}
entry.client.close();
sharedBrowserSurfaceRelayClients.delete(relayUrl);
};
export const EmbeddedSurface = ({ surface, relayUrl, className, style, fallback = null, onError, }) => {
const hostRef = useRef(null);
const unsubscribeRef = useRef(null);
const mountedHandleRef = useRef(null);
const loadedBundleUrlRef = useRef(null);
const propListenersRef = useRef(new Set());
const messageListenersRef = useRef(new Set());
const hostSessionIdRef = useRef("");
const [error, setError] = useState(null);
const resolvedRelayUrl = useMemo(() => relayUrl ?? defaultRelayUrlForBrowser(), [relayUrl]);
const relayClient = useMemo(() => acquireSharedBrowserSurfaceRelayClient(resolvedRelayUrl), [resolvedRelayUrl]);
const clearMountedSurface = () => {
try {
mountedHandleRef.current?.unmount();
}
catch {
// Ignore teardown races during remount.
}
mountedHandleRef.current = null;
loadedBundleUrlRef.current = null;
propListenersRef.current.clear();
messageListenersRef.current.clear();
};
const reportSurfaceError = (errorValue) => {
const error = toError(errorValue);
setError(error);
onError?.(error);
const details = getErrorDetails(error);
if (!hostSessionIdRef.current) {
return;
}
void relayClient.sendError(surface.stateContextId, surface.surfaceId, hostSessionIdRef.current, details).catch((reportingError) => {
console.error("[embedded-surface] failed to report surface error", reportingError);
});
};
const dispatchAction = (action) => {
if (!hostSessionIdRef.current) {
return;
}
void relayClient
.sendAction(surface.stateContextId, surface.surfaceId, hostSessionIdRef.current, action)
.catch((dispatchError) => {
reportSurfaceError(dispatchError);
});
};
const mountSurface = async (bundleUrl, initialProps) => {
const host = hostRef.current;
if (!host) {
throw new Error("Embedded surface host container is not mounted");
}
if (loadedBundleUrlRef.current !== bundleUrl) {
clearMountedSurface();
const imported = await import(/* @vite-ignore */ bundleUrl);
const surfaceModule = normalizeSurfaceModule(imported);
const mounted = await surfaceModule.mount({
container: host,
initialProps,
onProps: (listener) => {
propListenersRef.current.add(listener);
return () => {
propListenersRef.current.delete(listener);
};
},
onMessage: (listener) => {
messageListenersRef.current.add(listener);
return () => {
messageListenersRef.current.delete(listener);
};
},
dispatch: dispatchAction,
reportError: reportSurfaceError,
});
mountedHandleRef.current = {
unmount: mounted && typeof mounted.unmount === "function"
? () => mounted.unmount?.()
: () => { },
};
loadedBundleUrlRef.current = bundleUrl;
return;
}
for (const listener of propListenersRef.current) {
listener(initialProps);
}
};
const handleRelayMessage = async (message) => {
switch (message.type) {
case "browser-surface-bootstrap":
await mountSurface(message.bundleUrl, message.initialProps);
return;
case "browser-surface-props":
for (const listener of propListenersRef.current) {
listener(message.props);
}
return;
case "browser-surface-message":
for (const listener of messageListenersRef.current) {
listener(message.payload);
}
return;
case "browser-surface-closed":
clearMountedSurface();
return;
case "error":
reportSurfaceError(new Error(message.message));
return;
}
};
useLayoutEffect(() => {
setError(null);
clearMountedSurface();
let cancelled = false;
const hostSessionId = createBrowserSurfaceHostSessionId();
hostSessionIdRef.current = hostSessionId;
void relayClient
.attach(surface.stateContextId, surface.surfaceId, hostSessionId, async (message) => {
try {
await handleRelayMessage(message);
}
catch (messageError) {
reportSurfaceError(messageError);
}
})
.then(async (unsubscribe) => {
if (cancelled) {
await unsubscribe();
return;
}
unsubscribeRef.current = unsubscribe;
})
.catch((attachError) => {
if (!cancelled) {
reportSurfaceError(attachError);
}
});
return () => {
cancelled = true;
void unsubscribeRef.current?.();
unsubscribeRef.current = null;
hostSessionIdRef.current = "";
clearMountedSurface();
};
}, [relayClient, surface.packageName, surface.stateContextId, surface.surfaceId]);
useEffect(() => {
return () => {
releaseSharedBrowserSurfaceRelayClient(resolvedRelayUrl);
};
}, [relayClient, resolvedRelayUrl]);
if (error) {
return _jsx(_Fragment, { children: fallback });
}
return _jsx("div", { ref: hostRef, className: className, style: style });
};
export const createEmbeddedSurfaceComponent = (target, options) => {
const defaultSurfaceId = getEmbeddedSurfaceId(target, options?.surfaceId);
return ({ stateContextId, surfaceId, relayUrl, className, style, fallback, onError, }) => (_jsx(EmbeddedSurface, { surface: createEmbeddedSurfaceRef(target, {
stateContextId,
surfaceId: surfaceId ?? defaultSurfaceId,
}), relayUrl: relayUrl ?? options?.relayUrl, className: className, style: style, fallback: fallback, onError: onError }));
};
//# sourceMappingURL=browserSurfaceEmbedded.js.map
File diff suppressed because one or more lines are too long
+34
View File
@@ -0,0 +1,34 @@
import { type ComponentType } from "react";
import type { ZodType } from "zod";
export { EmbeddedSurface, createEmbeddedSurfaceComponent, type EmbeddedSurfaceComponentProps, } from "./browserSurfaceEmbedded.js";
export type BrowserSurfaceMountApi = {
container: HTMLElement;
initialProps: unknown;
onProps: (listener: (props: unknown) => void) => () => void;
onMessage: (listener: (message: unknown) => void) => () => void;
dispatch: (action: unknown) => void;
reportError?: (error: unknown) => void;
};
export type BrowserSurfaceViewProps<Props, Action> = {
props: Props;
dispatch: (action: Action) => void;
};
type CreateReactSurfaceMountOptions<Props, Action> = {
Component: ComponentType<BrowserSurfaceViewProps<Props, Action>>;
parseProps: (value: unknown) => Props;
normalizeAction: (action: Action) => Action;
onMessage?: (message: unknown) => void;
};
type CreateReactBrowserSurfaceMountOptions<Props, Action> = {
Component: ComponentType<BrowserSurfaceViewProps<Props, Action>>;
propsSchema: ZodType<Props>;
actionSchema: ZodType<Action>;
onMessage?: (message: unknown) => void;
};
export declare const createReactSurfaceMount: <Props, Action>({ Component, parseProps, normalizeAction, onMessage, }: CreateReactSurfaceMountOptions<Props, Action>) => ({ container, initialProps, onProps, onMessage: subscribeMessages, dispatch, reportError, }: BrowserSurfaceMountApi) => {
unmount: () => void;
};
export declare const createReactBrowserSurfaceMount: <Props, Action>({ Component, propsSchema, actionSchema, onMessage, }: CreateReactBrowserSurfaceMountOptions<Props, Action>) => ({ container, initialProps, onProps, onMessage: subscribeMessages, dispatch, reportError, }: BrowserSurfaceMountApi) => {
unmount: () => void;
};
//# sourceMappingURL=browserSurfaceReact.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browserSurfaceReact.d.ts","sourceRoot":"","sources":["../../src/browserSurfaceReact.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,aAAa,EAAkC,MAAM,OAAO,CAAC;AAEtF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACnC,OAAO,EACL,eAAe,EACf,8BAA8B,EAC9B,KAAK,6BAA6B,GACnC,MAAM,6BAA6B,CAAC;AAErC,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,WAAW,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC5D,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAChE,QAAQ,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,KAAK,EAAE,MAAM,IAAI;IACnD,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF,KAAK,8BAA8B,CAAC,KAAK,EAAE,MAAM,IAAI;IACnD,SAAS,EAAE,aAAa,CAAC,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,KAAK,CAAC;IACtC,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5C,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,CAAC;AAEF,KAAK,qCAAqC,CAAC,KAAK,EAAE,MAAM,IAAI;IAC1D,SAAS,EAAE,aAAa,CAAC,uBAAuB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC,CAAC;AA0CF,eAAO,MAAM,uBAAuB,GAAI,KAAK,EAAE,MAAM,EAAE,wDAKpD,8BAA8B,CAAC,KAAK,EAAE,MAAM,CAAC,MACtC,4FAOL,sBAAsB;;CA2E1B,CAAC;AAEF,eAAO,MAAM,8BAA8B,GAAI,KAAK,EAAE,MAAM,EAAE,sDAK3D,qCAAqC,CAAC,KAAK,EAAE,MAAM,CAAC,kGAlFlD,sBAAsB;;CAwFvB,CAAC"}
+94
View File
@@ -0,0 +1,94 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { Component } from "react";
import { createRoot } from "react-dom/client";
export { EmbeddedSurface, createEmbeddedSurfaceComponent, } from "./browserSurfaceEmbedded.js";
class SurfaceErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
this.props.onError(error, info);
}
componentDidUpdate(prevProps) {
if (this.state.hasError && prevProps.resetToken !== this.props.resetToken) {
this.setState({ hasError: false });
}
}
render() {
if (this.state.hasError) {
return null;
}
return this.props.children;
}
}
const surfaceRoots = new WeakMap();
export const createReactSurfaceMount = ({ Component, parseProps, normalizeAction, onMessage, }) => {
return ({ container, initialProps, onProps, onMessage: subscribeMessages, dispatch, reportError, }) => {
const root = surfaceRoots.get(container) ??
(() => {
const createdRoot = createRoot(container);
surfaceRoots.set(container, createdRoot);
return createdRoot;
})();
let currentProps = parseProps(initialProps);
let propsVersion = 0;
const reportSurfaceError = (error, componentStack) => {
if (!(error instanceof Error)) {
reportError?.(error);
return;
}
if (componentStack && componentStack.trim().length > 0) {
const errorWithComponentStack = new Error(error.message);
errorWithComponentStack.name = error.name;
errorWithComponentStack.stack = [
error.stack ?? `${error.name}: ${error.message}`,
"",
"Component stack:",
componentStack.trim(),
].join("\n");
reportError?.(errorWithComponentStack);
return;
}
reportError?.(error);
};
const render = () => {
root.render(_jsx(SurfaceErrorBoundary, { resetToken: propsVersion, onError: (error, info) => {
reportSurfaceError(error, info.componentStack ?? undefined);
}, children: _jsx(Component, { props: currentProps, dispatch: (action) => {
dispatch(normalizeAction(action));
} }) }));
};
render();
const unsubscribeProps = onProps((nextProps) => {
try {
currentProps = parseProps(nextProps);
propsVersion += 1;
render();
}
catch (error) {
reportSurfaceError(error);
}
});
const unsubscribeMessages = subscribeMessages((message) => {
onMessage?.(message);
});
return {
unmount: () => {
unsubscribeProps();
unsubscribeMessages();
root.unmount();
if (surfaceRoots.get(container) === root) {
surfaceRoots.delete(container);
}
},
};
};
};
export const createReactBrowserSurfaceMount = ({ Component, propsSchema, actionSchema, onMessage, }) => createReactSurfaceMount({
Component,
parseProps: (value) => propsSchema.parse(value),
normalizeAction: (action) => actionSchema.parse(action),
onMessage,
});
//# sourceMappingURL=browserSurfaceReact.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browserSurfaceReact.js","sourceRoot":"","sources":["../../src/browserSurfaceReact.tsx"],"names":[],"mappings":"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"}
+41
View File
@@ -0,0 +1,41 @@
import type { PackageSchema } from "./index.js";
import z from "zod";
export type JsonValue = null | boolean | number | string | JsonValue[] | {
[key: string]: JsonValue;
};
export declare const jsonValueSchema: z.ZodType<JsonValue>;
type BrowserSurfaceSchemaLike = PackageSchema & {
__quixos?: {
name?: string | null;
flakeRef?: string | null;
};
};
type BrowserSurfaceAnnotation = {
type: "quixos.ui.surface/v1";
surfaceId: string;
};
export type EmbeddedSurfaceRef<PackageName extends string = string, SurfaceId extends string = string> = {
packageName: PackageName;
stateContextId: string;
surfaceId: SurfaceId;
};
export declare const embeddedSurfaceRefSchema: z.ZodObject<{
packageName: z.ZodString;
stateContextId: z.ZodString;
surfaceId: z.ZodDefault<z.ZodString>;
}, z.z.core.$strip>;
export declare const createEmbeddedSurfaceRefSchema: <PackageName extends string = string, SurfaceId extends string = string>(target: string | BrowserSurfaceSchemaLike, options?: {
surfaceId?: SurfaceId;
}) => z.ZodObject<{
packageName: z.ZodLiteral<PackageName>;
stateContextId: z.ZodString;
surfaceId: z.ZodDefault<z.ZodString>;
}, z.z.core.$strip>;
export declare const createEmbeddedSurfaceRef: <PackageName extends string = string, SurfaceId extends string = string>(target: string | BrowserSurfaceSchemaLike, options: {
stateContextId: string;
surfaceId?: SurfaceId;
}) => EmbeddedSurfaceRef<PackageName, SurfaceId>;
export declare const getEmbeddedSurfacePackageName: (target: string | BrowserSurfaceSchemaLike) => string;
export declare const getEmbeddedSurfaceId: (target: string | BrowserSurfaceSchemaLike, surfaceId?: string) => string;
export type { BrowserSurfaceAnnotation };
//# sourceMappingURL=browserSurfaceShared.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browserSurfaceShared.d.ts","sourceRoot":"","sources":["../../src/browserSurfaceShared.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,CAAC,MAAM,KAAK,CAAC;AAEpB,MAAM,MAAM,SAAS,GACjB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEjC,eAAO,MAAM,eAAe,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,CAShD,CAAC;AAEF,KAAK,wBAAwB,GAAG,aAAa,GAAG;IAC9C,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;CACH,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC9B,IAAI,EAAE,sBAAsB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AA6EF,MAAM,MAAM,kBAAkB,CAC5B,WAAW,SAAS,MAAM,GAAG,MAAM,EACnC,SAAS,SAAS,MAAM,GAAG,MAAM,IAC/B;IACF,WAAW,EAAE,WAAW,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,SAAS,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,wBAAwB;;;;mBAInC,CAAC;AAEH,eAAO,MAAM,8BAA8B,GACzC,WAAW,SAAS,MAAM,GAAG,MAAM,EACnC,SAAS,SAAS,MAAM,GAAG,MAAM,EAEjC,QAAQ,MAAM,GAAG,wBAAwB,EACzC,UAAU;IACR,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;;;;mBAYF,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,WAAW,SAAS,MAAM,GAAG,MAAM,EACnC,SAAS,SAAS,MAAM,GAAG,MAAM,EAEjC,QAAQ,MAAM,GAAG,wBAAwB,EACzC,SAAS;IACP,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,KACA,kBAAkB,CAAC,WAAW,EAAE,SAAS,CAQ3C,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,QAAQ,MAAM,GAAG,wBAAwB,WACZ,CAAC;AAEhC,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,GAAG,wBAAwB,EACzC,YAAY,MAAM,WACoB,CAAC;AAEzC,YAAY,EAAE,wBAAwB,EAAE,CAAC"}
+94
View File
@@ -0,0 +1,94 @@
import z from "zod";
export const jsonValueSchema = z.lazy(() => z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.array(jsonValueSchema),
z.record(z.string(), jsonValueSchema),
]));
const browserSurfaceAnnotationSchema = z.object({
type: z.literal("quixos.ui.surface/v1"),
surfaceId: z.string().min(1),
});
const normalizePackageName = (value) => {
const normalized = value.startsWith("@quixos-package-schemas/")
? value.slice("@quixos-package-schemas/".length)
: value;
return normalized.endsWith(".git")
? normalized.slice(0, -".git".length)
: normalized;
};
const packageNameFromFlakeRef = (flakeRef) => {
const withoutQuery = flakeRef.split("?")[0] ?? flakeRef;
const withoutGitPrefix = withoutQuery.startsWith("git+")
? withoutQuery.slice(4)
: withoutQuery;
try {
const parsed = new URL(withoutGitPrefix);
const segments = parsed.pathname.split("/").filter(Boolean);
const packageName = segments.at(-1);
if (packageName) {
return normalizePackageName(packageName);
}
}
catch {
// Fall through to a simple path split for non-URL flake refs.
}
const segments = withoutGitPrefix.split("/").filter(Boolean);
const packageName = segments.at(-1);
if (!packageName) {
throw new Error(`Unable to determine package name for ${flakeRef}`);
}
return normalizePackageName(packageName);
};
const resolveSchemaPackageName = (schema) => {
if (typeof schema.__quixos?.name === "string" && schema.__quixos.name.length > 0) {
return normalizePackageName(schema.__quixos.name);
}
if (typeof schema.__quixos?.flakeRef === "string" &&
schema.__quixos.flakeRef.length > 0) {
return packageNameFromFlakeRef(schema.__quixos.flakeRef);
}
throw new Error("Package schema is missing __quixos metadata. Rebuild the schema with quixos helpers.");
};
const findDefaultSurfaceId = (schema) => {
const annotations = Array.isArray(schema.annotations) ? schema.annotations : [];
for (const annotation of annotations) {
const parsed = browserSurfaceAnnotationSchema.safeParse(annotation);
if (parsed.success) {
return parsed.data.surfaceId;
}
}
return "desktop";
};
const resolvePackageName = (target) => typeof target === "string"
? normalizePackageName(target)
: resolveSchemaPackageName(target);
const resolveSurfaceId = (target, surfaceId) => surfaceId ?? (typeof target === "string" ? "desktop" : findDefaultSurfaceId(target));
export const embeddedSurfaceRefSchema = z.object({
packageName: z.string().min(1),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1).default("desktop"),
});
export const createEmbeddedSurfaceRefSchema = (target, options) => {
const packageName = resolvePackageName(target);
const surfaceId = resolveSurfaceId(target, options?.surfaceId);
return z.object({
packageName: z.literal(packageName),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1).default(surfaceId),
});
};
export const createEmbeddedSurfaceRef = (target, options) => {
const packageName = resolvePackageName(target);
const surfaceId = resolveSurfaceId(target, options.surfaceId);
return {
packageName,
stateContextId: options.stateContextId,
surfaceId,
};
};
export const getEmbeddedSurfacePackageName = (target) => resolvePackageName(target);
export const getEmbeddedSurfaceId = (target, surfaceId) => resolveSurfaceId(target, surfaceId);
//# sourceMappingURL=browserSurfaceShared.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"browserSurfaceShared.js","sourceRoot":"","sources":["../../src/browserSurfaceShared.ts"],"names":[],"mappings":"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"}
+178
View File
@@ -0,0 +1,178 @@
import z, { type ZodTypeAny, type output as ZodOutput } from "zod";
import { type RuntimeErrorRequestParams } from "./rpc.js";
export * from "./rpc.js";
export type PayloadMode = "snapshot" | "delta";
export type SchemaAnnotation = {
type: string;
[key: string]: unknown;
};
export type PackageFunctionSchema = {
description: string;
annotations?: SchemaAnnotation[];
inputSchema: ZodTypeAny;
outputSchema: ZodTypeAny;
};
type StaticResourceSchema = {
description: string;
annotations?: SchemaAnnotation[];
dataSchema: ZodTypeAny;
history?: boolean;
payloadMode?: PayloadMode;
};
type ParameterizedResourceSchema = StaticResourceSchema & {
paramsSchema: ZodTypeAny;
};
export type StaticEventSchema = StaticResourceSchema;
export type ParameterizedEventSchema = ParameterizedResourceSchema;
export type EventSchema = StaticEventSchema | ParameterizedEventSchema;
export type EventSchemaMap = Record<string, EventSchema>;
export type StaticValueSchema = StaticResourceSchema;
export type ParameterizedValueSchema = ParameterizedResourceSchema;
export type ValueSchema = StaticValueSchema | ParameterizedValueSchema;
export type ValueSchemaMap = Record<string, ValueSchema>;
export type PackageSchema<Functions extends Record<string, PackageFunctionSchema> = Record<string, PackageFunctionSchema>, Events extends EventSchemaMap = EventSchemaMap, Values extends ValueSchemaMap = ValueSchemaMap> = {
schemaVersion: 1;
majorVersion: number;
description: string;
annotations?: SchemaAnnotation[];
functions: Functions;
events: Events;
values: Values;
};
export declare const QUIXOS_MEDIA_JSON_SCHEMA_KEY = "x-quixos-media";
export type QuixosDataUrlAnnotation = {
transport: "data-url";
mimeType: string;
extension?: string;
};
export declare const annotateDataUrlSchema: <Schema extends ZodTypeAny>(schema: Schema, params: {
mimeType: string;
extension?: string;
}) => Schema;
export declare const quixosMedia: {
dataUrlString: (params: {
mimeType: string;
extension?: string;
}) => z.ZodString;
annotateDataUrl: <Schema extends ZodTypeAny>(schema: Schema, params: {
mimeType: string;
extension?: string;
}) => Schema;
};
type FunctionInput<Schema extends PackageFunctionSchema> = ZodOutput<Schema["inputSchema"]>;
type FunctionOutput<Schema extends PackageFunctionSchema> = ZodOutput<Schema["outputSchema"]>;
type SchemaParams<Schema extends StaticResourceSchema | ParameterizedResourceSchema> = Schema extends {
paramsSchema: infer ParamsSchema extends ZodTypeAny;
} ? ZodOutput<ParamsSchema> : undefined;
type EventParams<Schema extends EventSchema> = SchemaParams<Schema>;
type EventData<Schema extends EventSchema> = ZodOutput<Schema["dataSchema"]>;
type ValueParams<Schema extends ValueSchema> = SchemaParams<Schema>;
type ValueData<Schema extends ValueSchema> = ZodOutput<Schema["dataSchema"]>;
export type SubscriptionOptions = {
signal?: AbortSignal;
subscriptionNamespace?: string;
};
export type SubscriptionHandle = {
unsubscribe: () => Promise<void>;
};
export type UsePackageOptions = {
contextNamespace?: string;
stateContextId?: string;
};
type ScopedResource<T> = T & {
withStateContext: (stateContextId: string) => T;
withContextNamespace: (contextNamespace?: string) => T;
};
type EventConsumer<Schema extends EventSchema> = (data: EventData<Schema>) => void | Promise<void>;
type ValueConsumer<Schema extends ValueSchema> = (data: ValueData<Schema>) => void | Promise<void>;
type StaticEventClientBase<Schema extends StaticEventSchema> = {
consume: (handler: EventConsumer<Schema>, options?: SubscriptionOptions) => Promise<SubscriptionHandle>;
};
type ParameterizedEventClientBase<Schema extends ParameterizedEventSchema> = {
consume: (params: EventParams<Schema>, handler: EventConsumer<Schema>, options?: SubscriptionOptions) => Promise<SubscriptionHandle>;
};
export type EventClient<Schema extends EventSchema> = Schema extends ParameterizedEventSchema ? ScopedResource<ParameterizedEventClientBase<Schema>> : ScopedResource<StaticEventClientBase<Schema & StaticEventSchema>>;
export type EventClients<Schema extends EventSchemaMap> = {
[Key in keyof Schema]: EventClient<Schema[Key]>;
};
type StaticValueClientBase<Schema extends StaticValueSchema> = {
get: () => Promise<ValueData<Schema>>;
watch: (handler: ValueConsumer<Schema>, options?: SubscriptionOptions) => Promise<SubscriptionHandle>;
};
type ParameterizedValueClientBase<Schema extends ParameterizedValueSchema> = {
get: (params: ValueParams<Schema>) => Promise<ValueData<Schema>>;
watch: (params: ValueParams<Schema>, handler: ValueConsumer<Schema>, options?: SubscriptionOptions) => Promise<SubscriptionHandle>;
};
export type ValueClient<Schema extends ValueSchema> = Schema extends ParameterizedValueSchema ? ScopedResource<ParameterizedValueClientBase<Schema>> : ScopedResource<StaticValueClientBase<Schema & StaticValueSchema>>;
export type ValueClients<Schema extends ValueSchemaMap> = {
[Key in keyof Schema]: ValueClient<Schema[Key]>;
};
export type PackageContext = {
stateContext: string;
stateDirectory: string;
usePackage: <Schema extends PackageSchema>(schema: Schema, options?: UsePackageOptions) => PackageClient<Schema>;
};
export type PackageFunction<Schema extends PackageFunctionSchema, Context = any> = (ctx: Context, params: FunctionInput<Schema>) => FunctionOutput<Schema> | Promise<FunctionOutput<Schema>>;
export type PackageFunctions<Functions extends Record<string, PackageFunctionSchema>, Context = any> = {
[Key in keyof Functions]: PackageFunction<Functions[Key], Context>;
};
export type EventSink<Schema extends EventSchema> = {
subscriptionId: string;
emit: (data: EventData<Schema>) => Promise<void>;
};
type EventHandler<Schema extends EventSchema, Context = any> = {
subscribe: (ctx: Context, params: EventParams<Schema>, sink: EventSink<Schema>) => void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
};
export type PackageEvents<Events extends EventSchemaMap, Context = any> = {
[Key in keyof Events]: EventHandler<Events[Key], Context>;
};
export type ValueSink<Schema extends ValueSchema> = {
subscriptionId: string;
set: (data: ValueData<Schema>) => Promise<void>;
};
type ValueHandler<Schema extends ValueSchema, Context = any> = {
get: (ctx: Context, params: ValueParams<Schema>) => ValueData<Schema> | Promise<ValueData<Schema>>;
watch?: (ctx: Context, params: ValueParams<Schema>, sink: ValueSink<Schema>) => void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
};
export type PackageValues<Values extends ValueSchemaMap, Context = any> = {
[Key in keyof Values]: ValueHandler<Values[Key], Context>;
};
export type PackageFunctionCaller<Schema extends PackageFunctionSchema> = ScopedResource<(params: FunctionInput<Schema>) => Promise<FunctionOutput<Schema>>>;
type PackageFunctionCallers<Functions extends Record<string, PackageFunctionSchema>> = {
[Key in keyof Functions]: PackageFunctionCaller<Functions[Key]>;
};
export type PackageClient<Schema extends PackageSchema> = {
functions: PackageFunctionCallers<Schema["functions"]>;
events: EventClients<Schema["events"]>;
values: ValueClients<Schema["values"]>;
withStateContext: (stateContextId: string) => PackageClient<Schema>;
withContextNamespace: (contextNamespace?: string) => PackageClient<Schema>;
};
export type CreatePackageOptions<Schema extends PackageSchema, Context extends PackageContext = PackageContext> = {
schema: Schema;
functions: PackageFunctions<Schema["functions"], Context>;
events: PackageEvents<Schema["events"], Context>;
values: PackageValues<Schema["values"], Context>;
onCreate?: () => void | Promise<void>;
onContextOpen?: (ctx: Context) => void | Promise<void>;
onContextClose?: (ctx: Context) => void | Promise<void>;
onDestroy?: () => void | Promise<void>;
};
type LifecycleManager = {};
export type RuntimeErrorPhase = RuntimeErrorRequestParams["phase"];
export type RuntimeErrorReportParams = {
phase: RuntimeErrorPhase;
error: unknown;
stateContextId?: string;
functionName?: string;
resourceKind?: "event" | "value";
resourceName?: string;
surfaceId?: string;
hostSessionId?: string;
stack?: string;
};
export declare const reportPackageRuntimeError: (params: RuntimeErrorReportParams) => Promise<void>;
export declare const runWithStateContext: <T>(stateContextId: string, fn: () => T | Promise<T>) => Promise<T>;
export declare const publishToSubscription: (subscriptionId: string, data: unknown) => Promise<void>;
export declare const createPackage: <Schema extends PackageSchema, Context extends PackageContext = PackageContext>(options: CreatePackageOptions<Schema, Context>) => LifecycleManager;
//# sourceMappingURL=index.d.ts.map
+1
View File
File diff suppressed because one or more lines are too long
+1154
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+280
View File
@@ -0,0 +1,280 @@
import z from "zod";
export declare const AuthMessageSchema: z.ZodObject<{
type: z.ZodLiteral<"auth">;
secret: z.ZodString;
pid: z.ZodOptional<z.ZodNumber>;
}, z.z.core.$strip>;
export declare const AuthAckMessageSchema: z.ZodObject<{
type: z.ZodLiteral<"auth-ack">;
}, z.z.core.$strip>;
export declare const RpcMethodSchema: z.ZodEnum<{
stop: "stop";
call: "call";
boot: "boot";
"target-context-open": "target-context-open";
"runtime-error": "runtime-error";
"context-open": "context-open";
"context-close": "context-close";
"event-subscribe": "event-subscribe";
"subscription-ready": "subscription-ready";
"event-unsubscribe": "event-unsubscribe";
"value-get": "value-get";
"value-watch": "value-watch";
"value-unwatch": "value-unwatch";
"subscription-publish": "subscription-publish";
}>;
export declare const RequestMessageSchema: z.ZodObject<{
type: z.ZodLiteral<"request">;
requestId: z.ZodString;
method: z.ZodEnum<{
stop: "stop";
call: "call";
boot: "boot";
"target-context-open": "target-context-open";
"runtime-error": "runtime-error";
"context-open": "context-open";
"context-close": "context-close";
"event-subscribe": "event-subscribe";
"subscription-ready": "subscription-ready";
"event-unsubscribe": "event-unsubscribe";
"value-get": "value-get";
"value-watch": "value-watch";
"value-unwatch": "value-unwatch";
"subscription-publish": "subscription-publish";
}>;
params: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, z.z.core.$strip>;
export declare const CallRequestParamsSchema: z.ZodObject<{
functionName: z.ZodString;
params: z.ZodUnknown;
target: z.ZodOptional<z.ZodString>;
targetPackageName: z.ZodOptional<z.ZodString>;
callerStateContextId: z.ZodOptional<z.ZodString>;
stateContextId: z.ZodOptional<z.ZodString>;
contextNamespace: z.ZodOptional<z.ZodString>;
}, z.z.core.$strip>;
export declare const BootRequestParamsSchema: z.ZodObject<{
target: z.ZodString;
}, z.z.core.$strip>;
export declare const TargetContextOpenRequestParamsSchema: z.ZodObject<{
target: z.ZodString;
targetPackageName: z.ZodOptional<z.ZodString>;
callerStateContextId: z.ZodOptional<z.ZodString>;
stateContextId: z.ZodOptional<z.ZodString>;
contextNamespace: z.ZodOptional<z.ZodString>;
}, z.z.core.$strip>;
export declare const RuntimeErrorRequestParamsSchema: z.ZodObject<{
phase: z.ZodEnum<{
function: "function";
"event-subscribe": "event-subscribe";
"value-get": "value-get";
"value-watch": "value-watch";
"subscription-publish": "subscription-publish";
"on-create": "on-create";
"on-destroy": "on-destroy";
"on-context-open": "on-context-open";
"on-context-close": "on-context-close";
"browser-surface": "browser-surface";
"event-cleanup": "event-cleanup";
"value-cleanup": "value-cleanup";
internal: "internal";
}>;
stateContextId: z.ZodOptional<z.ZodString>;
functionName: z.ZodOptional<z.ZodString>;
resourceKind: z.ZodOptional<z.ZodEnum<{
value: "value";
event: "event";
}>>;
resourceName: z.ZodOptional<z.ZodString>;
surfaceId: z.ZodOptional<z.ZodString>;
hostSessionId: z.ZodOptional<z.ZodString>;
message: z.ZodString;
stack: z.ZodOptional<z.ZodString>;
}, z.z.core.$strip>;
export declare const ContextOpenRequestParamsSchema: z.ZodObject<{
stateContextId: z.ZodString;
}, z.z.core.$strip>;
export declare const ContextCloseRequestParamsSchema: z.ZodObject<{
stateContextId: z.ZodString;
}, z.z.core.$strip>;
export declare const EventSubscribeRequestParamsSchema: z.ZodObject<{
eventName: z.ZodString;
params: z.ZodOptional<z.ZodUnknown>;
subscriptionNamespace: z.ZodOptional<z.ZodString>;
target: z.ZodOptional<z.ZodString>;
targetPackageName: z.ZodOptional<z.ZodString>;
callerStateContextId: z.ZodOptional<z.ZodString>;
stateContextId: z.ZodOptional<z.ZodString>;
contextNamespace: z.ZodOptional<z.ZodString>;
}, z.z.core.$strip>;
export declare const EventUnsubscribeRequestParamsSchema: z.ZodObject<{
subscriptionId: z.ZodString;
}, z.z.core.$strip>;
export declare const SubscriptionReadyRequestParamsSchema: z.ZodObject<{
subscriptionId: z.ZodString;
}, z.z.core.$strip>;
export declare const ValueGetRequestParamsSchema: z.ZodObject<{
valueName: z.ZodString;
params: z.ZodOptional<z.ZodUnknown>;
target: z.ZodOptional<z.ZodString>;
targetPackageName: z.ZodOptional<z.ZodString>;
callerStateContextId: z.ZodOptional<z.ZodString>;
stateContextId: z.ZodOptional<z.ZodString>;
contextNamespace: z.ZodOptional<z.ZodString>;
}, z.z.core.$strip>;
export declare const ValueWatchRequestParamsSchema: z.ZodObject<{
valueName: z.ZodString;
params: z.ZodOptional<z.ZodUnknown>;
subscriptionNamespace: z.ZodOptional<z.ZodString>;
target: z.ZodOptional<z.ZodString>;
targetPackageName: z.ZodOptional<z.ZodString>;
callerStateContextId: z.ZodOptional<z.ZodString>;
stateContextId: z.ZodOptional<z.ZodString>;
contextNamespace: z.ZodOptional<z.ZodString>;
}, z.z.core.$strip>;
export declare const ValueUnwatchRequestParamsSchema: z.ZodObject<{
subscriptionId: z.ZodString;
}, z.z.core.$strip>;
export declare const SubscriptionPublishRequestParamsSchema: z.ZodObject<{
subscriptionId: z.ZodString;
data: z.ZodUnknown;
}, z.z.core.$strip>;
export declare const SubscriptionResponseSchema: z.ZodObject<{
subscriptionId: z.ZodString;
}, z.z.core.$strip>;
export declare const SubscriptionDataMessageSchema: z.ZodObject<{
type: z.ZodLiteral<"subscription-data">;
subscriptionId: z.ZodString;
data: z.ZodUnknown;
}, z.z.core.$strip>;
export declare const ResponseOkMessageSchema: z.ZodObject<{
type: z.ZodLiteral<"response">;
requestId: z.ZodString;
ok: z.ZodLiteral<true>;
result: z.ZodOptional<z.ZodUnknown>;
}, z.z.core.$strip>;
export declare const ResponseErrorMessageSchema: z.ZodObject<{
type: z.ZodLiteral<"response">;
requestId: z.ZodString;
ok: z.ZodLiteral<false>;
error: z.ZodString;
}, z.z.core.$strip>;
export declare const ResponseMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
type: z.ZodLiteral<"response">;
requestId: z.ZodString;
ok: z.ZodLiteral<true>;
result: z.ZodOptional<z.ZodUnknown>;
}, z.z.core.$strip>, z.ZodObject<{
type: z.ZodLiteral<"response">;
requestId: z.ZodString;
ok: z.ZodLiteral<false>;
error: z.ZodString;
}, z.z.core.$strip>]>;
export declare const ServerMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
type: z.ZodLiteral<"auth-ack">;
}, z.z.core.$strip>, z.ZodObject<{
type: z.ZodLiteral<"request">;
requestId: z.ZodString;
method: z.ZodEnum<{
stop: "stop";
call: "call";
boot: "boot";
"target-context-open": "target-context-open";
"runtime-error": "runtime-error";
"context-open": "context-open";
"context-close": "context-close";
"event-subscribe": "event-subscribe";
"subscription-ready": "subscription-ready";
"event-unsubscribe": "event-unsubscribe";
"value-get": "value-get";
"value-watch": "value-watch";
"value-unwatch": "value-unwatch";
"subscription-publish": "subscription-publish";
}>;
params: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, z.z.core.$strip>, z.ZodUnion<readonly [z.ZodObject<{
type: z.ZodLiteral<"response">;
requestId: z.ZodString;
ok: z.ZodLiteral<true>;
result: z.ZodOptional<z.ZodUnknown>;
}, z.z.core.$strip>, z.ZodObject<{
type: z.ZodLiteral<"response">;
requestId: z.ZodString;
ok: z.ZodLiteral<false>;
error: z.ZodString;
}, z.z.core.$strip>]>, z.ZodObject<{
type: z.ZodLiteral<"subscription-data">;
subscriptionId: z.ZodString;
data: z.ZodUnknown;
}, z.z.core.$strip>]>;
export declare const ClientMessageSchema: z.ZodUnion<readonly [z.ZodObject<{
type: z.ZodLiteral<"auth">;
secret: z.ZodString;
pid: z.ZodOptional<z.ZodNumber>;
}, z.z.core.$strip>, z.ZodUnion<readonly [z.ZodObject<{
type: z.ZodLiteral<"response">;
requestId: z.ZodString;
ok: z.ZodLiteral<true>;
result: z.ZodOptional<z.ZodUnknown>;
}, z.z.core.$strip>, z.ZodObject<{
type: z.ZodLiteral<"response">;
requestId: z.ZodString;
ok: z.ZodLiteral<false>;
error: z.ZodString;
}, z.z.core.$strip>]>, z.ZodObject<{
type: z.ZodLiteral<"request">;
requestId: z.ZodString;
method: z.ZodEnum<{
stop: "stop";
call: "call";
boot: "boot";
"target-context-open": "target-context-open";
"runtime-error": "runtime-error";
"context-open": "context-open";
"context-close": "context-close";
"event-subscribe": "event-subscribe";
"subscription-ready": "subscription-ready";
"event-unsubscribe": "event-unsubscribe";
"value-get": "value-get";
"value-watch": "value-watch";
"value-unwatch": "value-unwatch";
"subscription-publish": "subscription-publish";
}>;
params: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, z.z.core.$strip>]>;
export type AuthMessage = z.infer<typeof AuthMessageSchema>;
export type AuthAckMessage = z.infer<typeof AuthAckMessageSchema>;
export type RequestMessage = z.infer<typeof RequestMessageSchema>;
export type CallRequestParams = z.infer<typeof CallRequestParamsSchema>;
export type BootRequestParams = z.infer<typeof BootRequestParamsSchema>;
export type TargetContextOpenRequestParams = z.infer<typeof TargetContextOpenRequestParamsSchema>;
export type RuntimeErrorRequestParams = z.infer<typeof RuntimeErrorRequestParamsSchema>;
export type ContextOpenRequestParams = z.infer<typeof ContextOpenRequestParamsSchema>;
export type ContextCloseRequestParams = z.infer<typeof ContextCloseRequestParamsSchema>;
export type EventSubscribeRequestParams = z.infer<typeof EventSubscribeRequestParamsSchema>;
export type EventUnsubscribeRequestParams = z.infer<typeof EventUnsubscribeRequestParamsSchema>;
export type SubscriptionReadyRequestParams = z.infer<typeof SubscriptionReadyRequestParamsSchema>;
export type ValueGetRequestParams = z.infer<typeof ValueGetRequestParamsSchema>;
export type ValueWatchRequestParams = z.infer<typeof ValueWatchRequestParamsSchema>;
export type ValueUnwatchRequestParams = z.infer<typeof ValueUnwatchRequestParamsSchema>;
export type SubscriptionPublishRequestParams = z.infer<typeof SubscriptionPublishRequestParamsSchema>;
export type SubscriptionResponse = z.infer<typeof SubscriptionResponseSchema>;
export type SubscriptionDataMessage = z.infer<typeof SubscriptionDataMessageSchema>;
export type ResponseMessage = z.infer<typeof ResponseMessageSchema>;
export type ClientMessage = z.infer<typeof ClientMessageSchema>;
export type ServerMessage = z.infer<typeof ServerMessageSchema>;
type SocketLike = {
send: (data: string) => void;
};
export declare const sendMessage: <Message extends ClientMessage | ServerMessage>(socket: SocketLike, message: Message) => void;
export declare const sendClientMessage: (socket: SocketLike, message: ClientMessage) => void;
export declare const sendServerMessage: (socket: SocketLike, message: ServerMessage) => void;
export declare const parseJson: (value: string) => {
ok: true;
value: any;
} | {
ok: false;
value?: undefined;
};
export {};
//# sourceMappingURL=rpc.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../src/rpc.ts"],"names":[],"mappings":"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"}
+165
View File
@@ -0,0 +1,165 @@
import z from "zod";
export const AuthMessageSchema = z.object({
type: z.literal("auth"),
secret: z.string().min(1),
pid: z.number().int().nonnegative().optional(),
});
export const AuthAckMessageSchema = z.object({
type: z.literal("auth-ack"),
});
export const RpcMethodSchema = z.enum([
"stop",
"call",
"boot",
"target-context-open",
"runtime-error",
"context-open",
"context-close",
"event-subscribe",
"subscription-ready",
"event-unsubscribe",
"value-get",
"value-watch",
"value-unwatch",
"subscription-publish",
]);
export const RequestMessageSchema = z.object({
type: z.literal("request"),
requestId: z.string().min(1),
method: RpcMethodSchema,
params: z.record(z.string(), z.unknown()),
});
const RoutedStateContextFields = {
target: z.string().min(1).optional(),
targetPackageName: z.string().min(1).optional(),
callerStateContextId: z.string().min(1).optional(),
stateContextId: z.string().min(1).optional(),
contextNamespace: z.string().min(1).optional(),
};
export const CallRequestParamsSchema = z.object({
...RoutedStateContextFields,
functionName: z.string().min(1),
params: z.unknown(),
});
export const BootRequestParamsSchema = z.object({
target: z.string().min(1),
});
export const TargetContextOpenRequestParamsSchema = z.object({
...RoutedStateContextFields,
target: z.string().min(1),
targetPackageName: z.string().min(1).optional(),
});
export const RuntimeErrorRequestParamsSchema = z.object({
phase: z.enum([
"on-create",
"on-destroy",
"on-context-open",
"on-context-close",
"browser-surface",
"function",
"event-subscribe",
"event-cleanup",
"value-get",
"value-watch",
"value-cleanup",
"subscription-publish",
"internal",
]),
stateContextId: z.string().min(1).optional(),
functionName: z.string().min(1).optional(),
resourceKind: z.enum(["event", "value"]).optional(),
resourceName: z.string().min(1).optional(),
surfaceId: z.string().min(1).optional(),
hostSessionId: z.string().min(1).optional(),
message: z.string().min(1),
stack: z.string().min(1).optional(),
});
export const ContextOpenRequestParamsSchema = z.object({
stateContextId: z.string().min(1),
});
export const ContextCloseRequestParamsSchema = z.object({
stateContextId: z.string().min(1),
});
export const EventSubscribeRequestParamsSchema = z.object({
...RoutedStateContextFields,
eventName: z.string().min(1),
params: z.unknown().optional(),
subscriptionNamespace: z.string().min(1).optional(),
});
export const EventUnsubscribeRequestParamsSchema = z.object({
subscriptionId: z.string().min(1),
});
export const SubscriptionReadyRequestParamsSchema = z.object({
subscriptionId: z.string().min(1),
});
export const ValueGetRequestParamsSchema = z.object({
...RoutedStateContextFields,
valueName: z.string().min(1),
params: z.unknown().optional(),
});
export const ValueWatchRequestParamsSchema = z.object({
...RoutedStateContextFields,
valueName: z.string().min(1),
params: z.unknown().optional(),
subscriptionNamespace: z.string().min(1).optional(),
});
export const ValueUnwatchRequestParamsSchema = z.object({
subscriptionId: z.string().min(1),
});
export const SubscriptionPublishRequestParamsSchema = z.object({
subscriptionId: z.string().min(1),
data: z.unknown(),
});
export const SubscriptionResponseSchema = z.object({
subscriptionId: z.string().min(1),
});
export const SubscriptionDataMessageSchema = z.object({
type: z.literal("subscription-data"),
subscriptionId: z.string().min(1),
data: z.unknown(),
});
export const ResponseOkMessageSchema = z.object({
type: z.literal("response"),
requestId: z.string().min(1),
ok: z.literal(true),
result: z.unknown().optional(),
});
export const ResponseErrorMessageSchema = z.object({
type: z.literal("response"),
requestId: z.string().min(1),
ok: z.literal(false),
error: z.string(),
});
export const ResponseMessageSchema = z.union([
ResponseOkMessageSchema,
ResponseErrorMessageSchema,
]);
export const ServerMessageSchema = z.union([
AuthAckMessageSchema,
RequestMessageSchema,
ResponseMessageSchema,
SubscriptionDataMessageSchema,
]);
export const ClientMessageSchema = z.union([
AuthMessageSchema,
ResponseMessageSchema,
RequestMessageSchema,
]);
export const sendMessage = (socket, message) => {
socket.send(JSON.stringify(message));
};
export const sendClientMessage = (socket, message) => {
sendMessage(socket, message);
};
export const sendServerMessage = (socket, message) => {
sendMessage(socket, message);
};
export const parseJson = (value) => {
try {
return { ok: true, value: JSON.parse(value) };
}
catch {
return { ok: false };
}
};
//# sourceMappingURL=rpc.js.map
+1
View File
File diff suppressed because one or more lines are too long
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@quixos/package-runtime",
"version": "1.0.0",
"author": "Timothy J. Aveni <me@timothyaveni.com> (https://timothyaveni.com/)",
"description": "",
"packageManager": "yarn@4.12.0",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
},
"./browser-surface": {
"types": "./dist/src/browserSurface.d.ts",
"default": "./dist/src/browserSurface.js"
},
"./browser-surface/shared": {
"types": "./dist/src/browserSurfaceShared.d.ts",
"default": "./dist/src/browserSurfaceShared.js"
},
"./browser-surface/react": {
"types": "./dist/src/browserSurfaceReact.d.ts",
"default": "./dist/src/browserSurfaceReact.js"
}
},
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@types/node": "^24",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/ws": "^8.18.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.9.3",
"ws": "^8.18.3",
"zod": "^4.3.6"
}
}
+761
View File
@@ -0,0 +1,761 @@
import WebSocket from "ws";
import z, { type ZodType } from "zod";
import {
createEmbeddedSurfaceRef,
createEmbeddedSurfaceRefSchema,
embeddedSurfaceRefSchema,
getEmbeddedSurfaceId,
getEmbeddedSurfacePackageName,
jsonValueSchema,
type JsonValue,
} from "./browserSurfaceShared.js";
import {
createPackage,
reportPackageRuntimeError,
type CreatePackageOptions,
type EventSchemaMap,
type PackageEvents,
type PackageFunctionSchema,
type PackageFunctions,
type PackageContext,
type PackageSchema,
type PackageValues,
type SchemaAnnotation,
type ValueSchemaMap,
} from "./index.js";
export { jsonValueSchema, type JsonValue } from "./browserSurfaceShared.js";
export {
createEmbeddedSurfaceRef,
createEmbeddedSurfaceRefSchema,
embeddedSurfaceRefSchema,
getEmbeddedSurfaceId,
getEmbeddedSurfacePackageName,
type EmbeddedSurfaceRef,
} from "./browserSurfaceShared.js";
export type { SubscriptionHandle } from "./index.js";
export const stateContextResultSchema = z.object({
stateContextId: z.string().min(1),
});
export const createBrowserSurfaceAnnotation = ({
surfaceId,
namespaceProp = "quixosKey",
surfaces = ["desktop"],
capabilities = ["mouse", "keyboard"],
aspectRatioHint = "16:10",
propsFunction = "propsUpdate",
}: {
surfaceId: string;
namespaceProp?: string;
surfaces?: string[];
capabilities?: string[];
aspectRatioHint?: string;
propsFunction?: string;
}): SchemaAnnotation => ({
type: "quixos.ui.surface/v1",
surfaceId,
runtime: "browser-module",
transport: "relay",
propsFunction,
namespaceProp,
surfaces,
capabilities,
aspectRatioHint,
});
export const createBrowserSurfacePropsUpdateFunctionSchema = <
PropsSchema extends z.ZodTypeAny,
>({
propsSchema,
namespaceProp = "quixosKey",
description,
inputDescription,
outputDescription,
}: {
propsSchema: PropsSchema;
namespaceProp?: string;
description: string;
inputDescription?: string;
outputDescription?: string;
}): PackageFunctionSchema => ({
description,
annotations: [
{
type: "quixos.ui.browser-surface-props/v1",
namespaceProp,
semantics: "replace",
transport: "relay",
},
],
inputSchema: (inputDescription
? z.object({ props: propsSchema }).describe(inputDescription)
: z.object({ props: propsSchema })) as z.ZodTypeAny,
outputSchema: (outputDescription
? stateContextResultSchema.describe(outputDescription)
: stateContextResultSchema) as z.ZodTypeAny,
});
export const createBrowserSurfacePackageSchema = <
PropsSchema extends z.ZodTypeAny,
ExtraFunctions extends Record<string, PackageFunctionSchema> = Record<
never,
never
>,
Events extends EventSchemaMap = Record<never, never>,
Values extends ValueSchemaMap = Record<never, never>,
>({
description,
majorVersion = 1,
surfaceId,
propsSchema,
namespaceProp = "quixosKey",
surfaces = ["desktop"],
capabilities = ["mouse", "keyboard"],
aspectRatioHint = "16:10",
propsFunctionDescription,
propsInputDescription,
propsOutputDescription,
annotations = [],
functions,
events,
values,
}: {
description: string;
majorVersion?: number;
surfaceId: string;
propsSchema: PropsSchema;
namespaceProp?: string;
surfaces?: string[];
capabilities?: string[];
aspectRatioHint?: string;
propsFunctionDescription: string;
propsInputDescription?: string;
propsOutputDescription?: string;
annotations?: SchemaAnnotation[];
functions?: ExtraFunctions;
events?: Events;
values?: Values;
}): PackageSchema<
{ propsUpdate: PackageFunctionSchema } & ExtraFunctions,
Events,
Values
> => ({
schemaVersion: 1,
majorVersion,
description,
annotations: [
createBrowserSurfaceAnnotation({
surfaceId,
namespaceProp,
surfaces,
capabilities,
aspectRatioHint,
propsFunction: "propsUpdate",
}),
...annotations,
],
functions: {
...(functions ?? ({} as ExtraFunctions)),
propsUpdate: createBrowserSurfacePropsUpdateFunctionSchema({
propsSchema,
namespaceProp,
description: propsFunctionDescription,
inputDescription: propsInputDescription,
outputDescription: propsOutputDescription,
}),
},
events: (events ?? {}) as Events,
values: (values ?? {}) as Values,
});
type EventSink<T> = {
emit: (data: T) => Promise<void>;
};
export type EventHub<Events extends Record<string, unknown>> = {
subscribe: <K extends keyof Events>(
eventName: K,
sink: EventSink<Events[K]>,
) => () => void;
emit: <K extends keyof Events>(
eventName: K,
payload: Events[K],
onError?: (error: unknown, eventName: K) => void,
) => void;
};
export const createEventHub = <
Events extends Record<string, unknown>,
>(): EventHub<Events> => {
const sinks = new Map<keyof Events, Set<EventSink<Events[keyof Events]>>>();
return {
subscribe: (eventName, sink) => {
let eventSinks = sinks.get(eventName);
if (!eventSinks) {
eventSinks = new Set();
sinks.set(eventName, eventSinks as Set<EventSink<Events[keyof Events]>>);
}
eventSinks.add(sink as EventSink<Events[keyof Events]>);
return () => {
eventSinks?.delete(sink as EventSink<Events[keyof Events]>);
};
},
emit: (eventName, payload, onError) => {
const eventSinks = sinks.get(eventName);
if (!eventSinks) {
return;
}
for (const sink of eventSinks) {
void sink
.emit(payload as Events[keyof Events])
.catch((error) => onError?.(error, eventName));
}
},
};
};
const DEFAULT_SURFACE_RELAY_URL = "ws://127.0.0.1:6247";
const producerMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("browser-surface-host-action"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
hostSessionId: z.string().min(1),
action: jsonValueSchema,
}),
z.object({
type: z.literal("browser-surface-host-detached"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
hostSessionId: z.string().min(1),
}),
z.object({
type: z.literal("browser-surface-host-error"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
hostSessionId: z.string().min(1),
message: z.string().min(1),
stack: z.string().min(1).optional(),
}),
z.object({
type: z.literal("error"),
message: z.string(),
}),
]);
type ProducerMessage =
| {
type: "browser-surface-host-action";
stateContextId: string;
surfaceId: string;
hostSessionId: string;
action: JsonValue;
}
| {
type: "browser-surface-host-detached";
stateContextId: string;
surfaceId: string;
hostSessionId: string;
}
| {
type: "browser-surface-host-error";
stateContextId: string;
surfaceId: string;
hostSessionId: string;
message: string;
stack?: string;
}
| {
type: "error";
message: string;
};
type ProducerClientMessage =
| {
type: "browser-surface-producer-attach";
stateContextId: string;
surfaceId: string;
}
| {
type: "browser-surface-producer-detach";
stateContextId: string;
surfaceId: string;
}
| {
type: "browser-surface-props";
stateContextId: string;
surfaceId: string;
props: unknown;
}
| {
type: "browser-surface-message";
stateContextId: string;
surfaceId: string;
payload: unknown;
};
type ProducerHandlers = {
onHostAction?: (hostSessionId: string, action: JsonValue) => void | Promise<void>;
onHostDetached?: (hostSessionId: string) => void | Promise<void>;
onHostError?: (
hostSessionId: string,
error: { message: string; stack?: string },
) => void | Promise<void>;
};
const surfaceKey = (stateContextId: string, surfaceId: string) =>
JSON.stringify([stateContextId, surfaceId]);
const getErrorMessage = (error: unknown) =>
error instanceof Error ? error.message : String(error);
const parseProducerMessage = (value: unknown): ProducerMessage | null => {
const parsed = producerMessageSchema.safeParse(value);
return parsed.success ? (parsed.data as ProducerMessage) : null;
};
export class BrowserSurfaceRelayProducerClient {
readonly relayUrl: string;
#socket: WebSocket | null = null;
#ready: Promise<void> | null = null;
#handlers = new Map<string, ProducerHandlers>();
constructor(relayUrl = DEFAULT_SURFACE_RELAY_URL) {
this.relayUrl = relayUrl;
}
async connect() {
if (this.#ready) {
return await this.#ready;
}
this.#ready = new Promise<void>((resolve, reject) => {
const socket = new WebSocket(this.relayUrl);
this.#socket = socket;
let opened = false;
const fail = (error: unknown) => {
const message = new Error(getErrorMessage(error));
this.#socket = null;
this.#ready = null;
if (!opened) {
reject(message);
return;
}
console.error(`[browser-surface] relay websocket disconnected: ${message.message}`);
};
socket.on("open", () => {
opened = true;
resolve();
});
socket.on("message", (rawData) => {
let parsedJson: unknown;
try {
parsedJson = JSON.parse(String(rawData));
} catch {
return;
}
const message = parseProducerMessage(parsedJson);
if (!message) {
return;
}
if (message.type === "error") {
console.error(`[browser-surface] relay error: ${message.message}`);
return;
}
const handlers = this.#handlers.get(
surfaceKey(message.stateContextId, message.surfaceId),
);
if (!handlers) {
return;
}
if (message.type === "browser-surface-host-action") {
void handlers.onHostAction?.(message.hostSessionId, message.action);
return;
}
if (message.type === "browser-surface-host-error") {
void handlers.onHostError?.(message.hostSessionId, {
message: message.message,
stack: message.stack,
});
return;
}
void handlers.onHostDetached?.(message.hostSessionId);
});
socket.on("error", () => {
fail(new Error("Surface relay websocket error"));
});
socket.on("close", () => {
fail(new Error("Surface relay websocket closed"));
});
});
return await this.#ready;
}
close() {
this.#socket?.close();
this.#socket = null;
this.#ready = null;
this.#handlers.clear();
}
async #send(message: ProducerClientMessage) {
await this.connect();
const socket = this.#socket;
if (!socket) {
throw new Error("Surface relay websocket is not connected");
}
socket.send(JSON.stringify(message));
}
async attach(
stateContextId: string,
surfaceId: string,
handlers: ProducerHandlers = {},
) {
this.#handlers.set(surfaceKey(stateContextId, surfaceId), handlers);
await this.#send({
type: "browser-surface-producer-attach",
stateContextId,
surfaceId,
});
}
async publishProps(stateContextId: string, surfaceId: string, props: unknown) {
await this.#send({
type: "browser-surface-props",
stateContextId,
surfaceId,
props,
});
}
async publishMessage(stateContextId: string, surfaceId: string, payload: unknown) {
await this.#send({
type: "browser-surface-message",
stateContextId,
surfaceId,
payload,
});
}
async detach(stateContextId: string, surfaceId: string) {
this.#handlers.delete(surfaceKey(stateContextId, surfaceId));
try {
await this.#send({
type: "browser-surface-producer-detach",
stateContextId,
surfaceId,
});
} catch {
// ignore shutdown races
}
}
}
type DependencyClient = ReturnType<PackageContext["usePackage"]>;
type RelaySchema = Parameters<PackageContext["usePackage"]>[0];
type BrowserSurfaceInstanceInternal<State> = {
ctx: PackageContext;
stateContextId: string;
relay: DependencyClient;
producer: BrowserSurfaceRelayProducerClient;
state: State;
publishQueue: Promise<void>;
publish: () => void;
publishMessage: (payload: unknown) => Promise<void>;
};
export type BrowserSurfaceInstance<State> = Omit<
BrowserSurfaceInstanceInternal<State>,
"publishQueue"
>;
type RelayRegistration = {
relayUrl: string;
};
export type BrowserSurfaceHostError = {
message: string;
stack?: string;
};
export type BrowserSurfaceControllerOptions<State, Props, Action> = {
relaySchema: RelaySchema;
surfaceId: string;
bundleDir: string;
entryPoint: string;
propsSchema: ZodType<Props>;
actionSchema: ZodType<Action>;
logPrefix: string;
createState: (
instance: BrowserSurfaceInstance<State>,
) => State | Promise<State>;
buildProps: (instance: BrowserSurfaceInstance<State>) => unknown;
applyProps: (
instance: BrowserSurfaceInstance<State>,
props: Props,
) => void | Promise<void>;
onHostAction?: (
instance: BrowserSurfaceInstance<State>,
hostSessionId: string,
action: Action,
) => void | Promise<void>;
onHostDetached?: (
instance: BrowserSurfaceInstance<State>,
hostSessionId: string,
) => void | Promise<void>;
onHostError?: (
instance: BrowserSurfaceInstance<State>,
hostSessionId: string,
error: BrowserSurfaceHostError,
) => void | Promise<void>;
onAfterCreate?: (
instance: BrowserSurfaceInstance<State>,
) => void | Promise<void>;
onBeforeDestroy?: (
instance: BrowserSurfaceInstance<State>,
) => void | Promise<void>;
};
type BrowserSurfaceController<Context extends PackageContext = PackageContext> = {
onContextOpen: (ctx: Context) => void | Promise<void>;
onContextClose: (ctx: Context) => void | Promise<void>;
onDestroy: () => void | Promise<void>;
propsUpdate: (ctx: Context, nextProps: unknown) => Promise<string>;
};
export const createBrowserSurfacePackage = <
Schema extends PackageSchema,
Context extends PackageContext = PackageContext,
>({
schema,
surface,
functions,
events,
values,
onCreate,
onContextOpen,
onContextClose,
onDestroy,
}: Omit<CreatePackageOptions<Schema, Context>, "functions"> & {
surface: BrowserSurfaceController<Context>;
functions?: Omit<PackageFunctions<Schema["functions"], Context>, "propsUpdate">;
}) =>
createPackage({
schema,
onCreate,
onContextOpen: async (ctx) => {
await surface.onContextOpen(ctx);
await onContextOpen?.(ctx);
},
onContextClose: async (ctx) => {
try {
await onContextClose?.(ctx);
} finally {
await surface.onContextClose(ctx);
}
},
onDestroy: async () => {
try {
await onDestroy?.();
} finally {
await surface.onDestroy();
}
},
functions: {
...(functions ?? ({} as Omit<PackageFunctions<Schema["functions"], Context>, "propsUpdate">)),
propsUpdate: async (ctx: Context, params: unknown) =>
stateContextResultSchema.parse({
stateContextId: await surface.propsUpdate(
ctx,
(params as { props: unknown }).props,
),
}),
} as PackageFunctions<Schema["functions"], Context>,
events: events as PackageEvents<Schema["events"], Context>,
values: values as PackageValues<Schema["values"], Context>,
});
const toPublicInstance = <State>(
instance: BrowserSurfaceInstanceInternal<State>,
): BrowserSurfaceInstance<State> => instance;
export const createBrowserSurfaceController = <State, Props, Action>({
relaySchema,
surfaceId,
bundleDir,
entryPoint,
propsSchema,
actionSchema,
logPrefix,
createState,
buildProps,
applyProps,
onHostAction,
onHostDetached,
onHostError,
onAfterCreate,
onBeforeDestroy,
}: BrowserSurfaceControllerOptions<State, Props, Action>) => {
const renderStates = new Map<string, BrowserSurfaceInstanceInternal<State>>();
const pendingRenderStates = new Map<
string,
Promise<BrowserSurfaceInstanceInternal<State>>
>();
const queuePublish = (instance: BrowserSurfaceInstanceInternal<State>) => {
instance.publishQueue = instance.publishQueue
.then(async () => {
await instance.producer.publishProps(
instance.stateContextId,
surfaceId,
buildProps(toPublicInstance(instance)),
);
})
.catch((error) => {
console.error(
`${logPrefix} failed to publish surface props for ${instance.stateContextId}`,
error,
);
});
};
const createRenderState = async (ctx: PackageContext) => {
const relay = ctx.usePackage(relaySchema);
const registration = (await (
relay.functions.registerBrowserSurface as (params: {
stateContextId: string;
surfaceId: string;
bundleDir: string;
entryPoint: string;
}) => Promise<RelayRegistration>
)({
stateContextId: ctx.stateContext,
surfaceId,
bundleDir,
entryPoint,
})) as RelayRegistration;
const producer = new BrowserSurfaceRelayProducerClient(registration.relayUrl);
const instance: BrowserSurfaceInstanceInternal<State> = {
ctx,
stateContextId: ctx.stateContext,
relay,
producer,
state: undefined as State,
publishQueue: Promise.resolve(),
publish: () => {
queuePublish(instance);
},
publishMessage: async (payload: unknown) => {
await producer.publishMessage(ctx.stateContext, surfaceId, payload);
},
};
instance.state = await createState(toPublicInstance(instance));
renderStates.set(ctx.stateContext, instance);
await producer.attach(ctx.stateContext, surfaceId, {
onHostAction: async (hostSessionId, action) => {
if (!onHostAction) {
return;
}
const parsed = actionSchema.parse(action);
await onHostAction(toPublicInstance(instance), hostSessionId, parsed);
},
onHostDetached: async (hostSessionId) => {
await onHostDetached?.(toPublicInstance(instance), hostSessionId);
},
onHostError: async (hostSessionId, error) => {
await reportPackageRuntimeError({
phase: "browser-surface",
error: new Error(error.message),
stateContextId: instance.stateContextId,
surfaceId,
hostSessionId,
stack: error.stack,
});
await onHostError?.(toPublicInstance(instance), hostSessionId, error);
},
});
await onAfterCreate?.(toPublicInstance(instance));
instance.publish();
return instance;
};
const getOrCreate = async (ctx: PackageContext) => {
const existing = renderStates.get(ctx.stateContext);
if (existing) {
return existing;
}
const pending = pendingRenderStates.get(ctx.stateContext);
if (pending) {
return await pending;
}
const creation = createRenderState(ctx);
pendingRenderStates.set(ctx.stateContext, creation);
try {
return await creation;
} finally {
pendingRenderStates.delete(ctx.stateContext);
}
};
const destroyState = async (stateContextId: string) => {
const instance = renderStates.get(stateContextId);
if (!instance) {
return;
}
renderStates.delete(stateContextId);
pendingRenderStates.delete(stateContextId);
await instance.publishQueue.catch(() => {});
await onBeforeDestroy?.(toPublicInstance(instance));
await instance.producer.detach(stateContextId, surfaceId);
instance.producer.close();
await (
instance.relay.functions.unregisterBrowserSurface as (params: {
stateContextId: string;
surfaceId: string;
}) => Promise<unknown>
)({ stateContextId, surfaceId });
};
return {
getOrCreate: async (ctx: PackageContext) =>
toPublicInstance(await getOrCreate(ctx)),
onContextOpen: async (ctx: PackageContext) => {
await getOrCreate(ctx);
},
onContextClose: async (ctx: PackageContext) => {
await destroyState(ctx.stateContext);
},
onDestroy: async () => {
const activeStateContexts = [...renderStates.keys()];
pendingRenderStates.clear();
for (const stateContextId of activeStateContexts) {
await destroyState(stateContextId);
}
},
propsUpdate: async (ctx: PackageContext, nextProps: unknown) => {
const parsedProps = propsSchema.parse(nextProps);
const instance = await getOrCreate(ctx);
await applyProps(toPublicInstance(instance), parsedProps);
instance.publish();
return ctx.stateContext;
},
};
};
+725
View File
@@ -0,0 +1,725 @@
import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
type CSSProperties,
type ReactNode,
} from "react";
import type { PackageSchema } from "./index.js";
import {
createEmbeddedSurfaceRef,
getEmbeddedSurfaceId,
type EmbeddedSurfaceRef,
} from "./browserSurfaceShared.js";
import {
jsonValueSchema,
} from "./browserSurfaceShared.js";
import z from "zod";
type BrowserSurfaceSchemaLike = PackageSchema & {
__quixos?: {
name?: string | null;
flakeRef?: string | null;
};
};
type SurfaceModuleHandle = {
unmount?: () => void;
} | void;
type SurfaceMountApi = {
container: HTMLElement;
initialProps: unknown;
onProps: (listener: (props: unknown) => void) => () => void;
onMessage: (listener: (message: unknown) => void) => () => void;
dispatch: (action: unknown) => void;
reportError?: (error: unknown) => void;
};
type SurfaceModule = {
mount: (api: SurfaceMountApi) => SurfaceModuleHandle | Promise<SurfaceModuleHandle>;
};
const LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
const RELAY_LOCAL_PORT = "6247";
const RELAY_PROXY_PATH = "/relay/";
const relayServerMessageSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("browser-surface-bootstrap"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
bundleUrl: z.string().url(),
initialProps: jsonValueSchema,
}),
z.object({
type: z.literal("browser-surface-props"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
props: jsonValueSchema,
}),
z.object({
type: z.literal("browser-surface-message"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
payload: jsonValueSchema,
}),
z.object({
type: z.literal("browser-surface-closed"),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1),
}),
z.object({
type: z.literal("error"),
message: z.string(),
}),
]);
type BrowserSurfaceRelayServerMessage =
| {
type: "browser-surface-bootstrap";
stateContextId: string;
surfaceId: string;
bundleUrl: string;
initialProps: unknown;
}
| {
type: "browser-surface-props";
stateContextId: string;
surfaceId: string;
props: unknown;
}
| {
type: "browser-surface-message";
stateContextId: string;
surfaceId: string;
payload: unknown;
}
| {
type: "browser-surface-closed";
stateContextId: string;
surfaceId: string;
}
| {
type: "error";
message: string;
};
type BrowserSurfaceRelayClientMessage =
| {
type: "browser-surface-host-attach";
stateContextId: string;
surfaceId: string;
hostSessionId: string;
}
| {
type: "browser-surface-host-detach";
stateContextId: string;
surfaceId: string;
hostSessionId: string;
}
| {
type: "browser-surface-host-action";
stateContextId: string;
surfaceId: string;
hostSessionId: string;
action: unknown;
}
| {
type: "browser-surface-host-error";
stateContextId: string;
surfaceId: string;
hostSessionId: string;
message: string;
stack?: string;
};
type HostAttachment = {
stateContextId: string;
surfaceId: string;
hostSessionId: string;
handler: (message: BrowserSurfaceRelayServerMessage) => void | Promise<void>;
};
type SharedBrowserSurfaceRelayClientEntry = {
client: BrowserSurfaceRelayClient;
refCount: number;
};
type EmbeddedSurfaceHostProps = {
surface: EmbeddedSurfaceRef;
relayUrl?: string;
className?: string;
style?: CSSProperties;
fallback?: ReactNode;
onError?: (error: Error) => void;
};
export type EmbeddedSurfaceComponentProps<SurfaceId extends string = string> = {
stateContextId: string;
surfaceId?: SurfaceId;
relayUrl?: string;
className?: string;
style?: CSSProperties;
fallback?: ReactNode;
onError?: (error: Error) => void;
};
const isLocalHostname = (hostname: string) => LOCAL_HOSTNAMES.has(hostname);
const urlForCurrentHostAndPort = (
locationLike: { href: string } = window.location,
port: string,
{ ws = false }: { ws?: boolean } = {},
) => {
const url = new URL(locationLike.href);
url.protocol = ws
? url.protocol === "https:"
? "wss:"
: "ws:"
: url.protocol === "https:"
? "https:"
: "http:";
url.port = port;
url.pathname = "/";
url.search = "";
url.hash = "";
return url.toString();
};
const urlForCurrentOriginAndPath = (
locationLike: { href: string } = window.location,
path: string,
{ ws = false }: { ws?: boolean } = {},
) => {
const url = new URL(locationLike.href);
url.protocol = ws
? url.protocol === "https:"
? "wss:"
: "ws:"
: url.protocol === "https:"
? "https:"
: "http:";
url.port = "";
url.pathname = path;
url.search = "";
url.hash = "";
return url.toString();
};
const defaultRelayUrlForBrowser = (
locationLike: Pick<Location, "hostname" | "href"> = window.location,
) => {
if (isLocalHostname(locationLike.hostname)) {
return urlForCurrentHostAndPort(locationLike, RELAY_LOCAL_PORT, {
ws: true,
});
}
return urlForCurrentOriginAndPath(locationLike, RELAY_PROXY_PATH, {
ws: true,
});
};
const getErrorMessage = (error: unknown) =>
error instanceof Error ? error.message : String(error);
const toError = (error: unknown) =>
error instanceof Error ? error : new Error(String(error));
const getErrorDetails = (error: unknown) => {
if (error instanceof Error) {
return {
message: error.message || "Unknown error",
stack: error.stack,
};
}
return {
message: String(error),
stack: undefined,
};
};
const parseServerMessage = (
value: unknown,
): BrowserSurfaceRelayServerMessage | null => {
const parsed = relayServerMessageSchema.safeParse(value);
return parsed.success ? (parsed.data as BrowserSurfaceRelayServerMessage) : null;
};
const normalizeSurfaceModule = (module: unknown): SurfaceModule => {
if (
typeof module === "object" &&
module !== null &&
typeof (module as { mount?: unknown }).mount === "function"
) {
return module as SurfaceModule;
}
throw new Error("Surface bundle does not export a mount(...) function");
};
const attachmentKey = (
stateContextId: string,
surfaceId: string,
hostSessionId: string,
) => JSON.stringify([stateContextId, surfaceId, hostSessionId]);
const sharedBrowserSurfaceRelayClients = new Map<
string,
SharedBrowserSurfaceRelayClientEntry
>();
const createBrowserSurfaceHostSessionId = () => crypto.randomUUID();
class BrowserSurfaceRelayClient {
readonly relayUrl: string;
#socket: WebSocket | null = null;
#ready: Promise<void> | null = null;
#attachments = new Map<string, HostAttachment>();
constructor(relayUrl: string) {
this.relayUrl = relayUrl;
}
async connect() {
if (this.#ready) {
return await this.#ready;
}
this.#ready = new Promise<void>((resolve, reject) => {
const socket = new WebSocket(this.relayUrl);
this.#socket = socket;
let opened = false;
const fail = (error: unknown) => {
const message = toError(error);
this.#socket = null;
this.#ready = null;
if (!opened) {
reject(message);
}
};
socket.addEventListener("open", () => {
opened = true;
resolve();
});
socket.addEventListener("message", (event) => {
let parsedJson: unknown;
try {
parsedJson = JSON.parse(String(event.data));
} catch {
return;
}
const message = parseServerMessage(parsedJson);
if (!message) {
return;
}
if (message.type === "error") {
console.error(`[browser-surface-relay] ${message.message}`);
return;
}
const attachment = this.#findAttachment(
message.stateContextId,
message.surfaceId,
);
if (!attachment) {
return;
}
void attachment.handler(message);
});
socket.addEventListener("error", () => {
fail(new Error("Browser surface relay socket error"));
});
socket.addEventListener("close", () => {
fail(new Error("Browser surface relay socket closed"));
});
});
return await this.#ready;
}
close() {
this.#socket?.close();
this.#socket = null;
this.#ready = null;
this.#attachments.clear();
}
async #send(message: BrowserSurfaceRelayClientMessage) {
await this.connect();
const socket = this.#socket;
if (!socket) {
throw new Error("Browser surface relay socket is not connected");
}
socket.send(JSON.stringify(message));
}
#findAttachment(stateContextId: string, surfaceId: string) {
for (const attachment of this.#attachments.values()) {
if (
attachment.stateContextId === stateContextId &&
attachment.surfaceId === surfaceId
) {
return attachment;
}
}
return null;
}
async attach(
stateContextId: string,
surfaceId: string,
hostSessionId: string,
handler: (message: BrowserSurfaceRelayServerMessage) => void | Promise<void>,
) {
const key = attachmentKey(stateContextId, surfaceId, hostSessionId);
const existing = this.#findAttachment(stateContextId, surfaceId);
if (existing) {
for (const [existingKey, attachment] of this.#attachments.entries()) {
if (attachment === existing) {
this.#attachments.delete(existingKey);
break;
}
}
try {
await this.#send({
type: "browser-surface-host-detach",
stateContextId,
surfaceId,
hostSessionId: existing.hostSessionId,
});
} catch {
// Ignore replacement races.
}
}
this.#attachments.set(key, {
stateContextId,
surfaceId,
hostSessionId,
handler,
});
await this.#send({
type: "browser-surface-host-attach",
stateContextId,
surfaceId,
hostSessionId,
});
return async () => {
this.#attachments.delete(key);
try {
await this.#send({
type: "browser-surface-host-detach",
stateContextId,
surfaceId,
hostSessionId,
});
} catch {
// Ignore detach races during teardown.
}
};
}
async sendAction(
stateContextId: string,
surfaceId: string,
hostSessionId: string,
action: unknown,
) {
await this.#send({
type: "browser-surface-host-action",
stateContextId,
surfaceId,
hostSessionId,
action,
});
}
async sendError(
stateContextId: string,
surfaceId: string,
hostSessionId: string,
error: {
message: string;
stack?: string;
},
) {
await this.#send({
type: "browser-surface-host-error",
stateContextId,
surfaceId,
hostSessionId,
message: error.message,
...(error.stack ? { stack: error.stack } : {}),
});
}
}
const acquireSharedBrowserSurfaceRelayClient = (relayUrl: string) => {
const existing = sharedBrowserSurfaceRelayClients.get(relayUrl);
if (existing) {
existing.refCount += 1;
return existing.client;
}
const client = new BrowserSurfaceRelayClient(relayUrl);
sharedBrowserSurfaceRelayClients.set(relayUrl, {
client,
refCount: 1,
});
return client;
};
const releaseSharedBrowserSurfaceRelayClient = (relayUrl: string) => {
const entry = sharedBrowserSurfaceRelayClients.get(relayUrl);
if (!entry) {
return;
}
entry.refCount -= 1;
if (entry.refCount > 0) {
return;
}
entry.client.close();
sharedBrowserSurfaceRelayClients.delete(relayUrl);
};
export const EmbeddedSurface = ({
surface,
relayUrl,
className,
style,
fallback = null,
onError,
}: EmbeddedSurfaceHostProps) => {
const hostRef = useRef<HTMLDivElement | null>(null);
const unsubscribeRef = useRef<null | (() => Promise<void>)>(null);
const mountedHandleRef = useRef<{ unmount: () => void } | null>(null);
const loadedBundleUrlRef = useRef<string | null>(null);
const propListenersRef = useRef(new Set<(props: unknown) => void>());
const messageListenersRef = useRef(new Set<(message: unknown) => void>());
const hostSessionIdRef = useRef("");
const [error, setError] = useState<Error | null>(null);
const resolvedRelayUrl = useMemo(
() => relayUrl ?? defaultRelayUrlForBrowser(),
[relayUrl],
);
const relayClient = useMemo(
() => acquireSharedBrowserSurfaceRelayClient(resolvedRelayUrl),
[resolvedRelayUrl],
);
const clearMountedSurface = () => {
try {
mountedHandleRef.current?.unmount();
} catch {
// Ignore teardown races during remount.
}
mountedHandleRef.current = null;
loadedBundleUrlRef.current = null;
propListenersRef.current.clear();
messageListenersRef.current.clear();
};
const reportSurfaceError = (errorValue: unknown) => {
const error = toError(errorValue);
setError(error);
onError?.(error);
const details = getErrorDetails(error);
if (!hostSessionIdRef.current) {
return;
}
void relayClient.sendError(
surface.stateContextId,
surface.surfaceId,
hostSessionIdRef.current,
details,
).catch((reportingError) => {
console.error(
"[embedded-surface] failed to report surface error",
reportingError,
);
});
};
const dispatchAction = (action: unknown) => {
if (!hostSessionIdRef.current) {
return;
}
void relayClient
.sendAction(
surface.stateContextId,
surface.surfaceId,
hostSessionIdRef.current,
action,
)
.catch((dispatchError) => {
reportSurfaceError(dispatchError);
});
};
const mountSurface = async (bundleUrl: string, initialProps: unknown) => {
const host = hostRef.current;
if (!host) {
throw new Error("Embedded surface host container is not mounted");
}
if (loadedBundleUrlRef.current !== bundleUrl) {
clearMountedSurface();
const imported = await import(/* @vite-ignore */ bundleUrl);
const surfaceModule = normalizeSurfaceModule(imported);
const mounted = await surfaceModule.mount({
container: host,
initialProps,
onProps: (listener) => {
propListenersRef.current.add(listener);
return () => {
propListenersRef.current.delete(listener);
};
},
onMessage: (listener) => {
messageListenersRef.current.add(listener);
return () => {
messageListenersRef.current.delete(listener);
};
},
dispatch: dispatchAction,
reportError: reportSurfaceError,
});
mountedHandleRef.current = {
unmount:
mounted && typeof mounted.unmount === "function"
? () => mounted.unmount?.()
: () => {},
};
loadedBundleUrlRef.current = bundleUrl;
return;
}
for (const listener of propListenersRef.current) {
listener(initialProps);
}
};
const handleRelayMessage = async (message: BrowserSurfaceRelayServerMessage) => {
switch (message.type) {
case "browser-surface-bootstrap":
await mountSurface(message.bundleUrl, message.initialProps);
return;
case "browser-surface-props":
for (const listener of propListenersRef.current) {
listener(message.props);
}
return;
case "browser-surface-message":
for (const listener of messageListenersRef.current) {
listener(message.payload);
}
return;
case "browser-surface-closed":
clearMountedSurface();
return;
case "error":
reportSurfaceError(new Error(message.message));
return;
}
};
useLayoutEffect(() => {
setError(null);
clearMountedSurface();
let cancelled = false;
const hostSessionId = createBrowserSurfaceHostSessionId();
hostSessionIdRef.current = hostSessionId;
void relayClient
.attach(
surface.stateContextId,
surface.surfaceId,
hostSessionId,
async (message) => {
try {
await handleRelayMessage(message);
} catch (messageError) {
reportSurfaceError(messageError);
}
},
)
.then(async (unsubscribe) => {
if (cancelled) {
await unsubscribe();
return;
}
unsubscribeRef.current = unsubscribe;
})
.catch((attachError) => {
if (!cancelled) {
reportSurfaceError(attachError);
}
});
return () => {
cancelled = true;
void unsubscribeRef.current?.();
unsubscribeRef.current = null;
hostSessionIdRef.current = "";
clearMountedSurface();
};
}, [relayClient, surface.packageName, surface.stateContextId, surface.surfaceId]);
useEffect(() => {
return () => {
releaseSharedBrowserSurfaceRelayClient(resolvedRelayUrl);
};
}, [relayClient, resolvedRelayUrl]);
if (error) {
return <>{fallback}</>;
}
return <div ref={hostRef} className={className} style={style} />;
};
export const createEmbeddedSurfaceComponent = <
Target extends string | BrowserSurfaceSchemaLike,
SurfaceId extends string = string,
>(
target: Target,
options?: {
surfaceId?: SurfaceId;
relayUrl?: string;
},
) => {
const defaultSurfaceId = getEmbeddedSurfaceId(
target,
options?.surfaceId,
) as SurfaceId;
return ({
stateContextId,
surfaceId,
relayUrl,
className,
style,
fallback,
onError,
}: EmbeddedSurfaceComponentProps<SurfaceId>) => (
<EmbeddedSurface
surface={createEmbeddedSurfaceRef(target, {
stateContextId,
surfaceId: surfaceId ?? defaultSurfaceId,
})}
relayUrl={relayUrl ?? options?.relayUrl}
className={className}
style={style}
fallback={fallback}
onError={onError}
/>
);
};
+179
View File
@@ -0,0 +1,179 @@
import { Component, type ComponentType, type ErrorInfo, type ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";
import type { ZodType } from "zod";
export {
EmbeddedSurface,
createEmbeddedSurfaceComponent,
type EmbeddedSurfaceComponentProps,
} from "./browserSurfaceEmbedded.js";
export type BrowserSurfaceMountApi = {
container: HTMLElement;
initialProps: unknown;
onProps: (listener: (props: unknown) => void) => () => void;
onMessage: (listener: (message: unknown) => void) => () => void;
dispatch: (action: unknown) => void;
reportError?: (error: unknown) => void;
};
export type BrowserSurfaceViewProps<Props, Action> = {
props: Props;
dispatch: (action: Action) => void;
};
type CreateReactSurfaceMountOptions<Props, Action> = {
Component: ComponentType<BrowserSurfaceViewProps<Props, Action>>;
parseProps: (value: unknown) => Props;
normalizeAction: (action: Action) => Action;
onMessage?: (message: unknown) => void;
};
type CreateReactBrowserSurfaceMountOptions<Props, Action> = {
Component: ComponentType<BrowserSurfaceViewProps<Props, Action>>;
propsSchema: ZodType<Props>;
actionSchema: ZodType<Action>;
onMessage?: (message: unknown) => void;
};
type SurfaceErrorBoundaryProps = {
children: ReactNode;
onError: (error: Error, info: ErrorInfo) => void;
resetToken: number;
};
type SurfaceErrorBoundaryState = {
hasError: boolean;
};
class SurfaceErrorBoundary extends Component<
SurfaceErrorBoundaryProps,
SurfaceErrorBoundaryState
> {
state: SurfaceErrorBoundaryState = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError(error, info);
}
componentDidUpdate(prevProps: SurfaceErrorBoundaryProps) {
if (this.state.hasError && prevProps.resetToken !== this.props.resetToken) {
this.setState({ hasError: false });
}
}
render() {
if (this.state.hasError) {
return null;
}
return this.props.children;
}
}
const surfaceRoots = new WeakMap<HTMLElement, Root>();
export const createReactSurfaceMount = <Props, Action>({
Component,
parseProps,
normalizeAction,
onMessage,
}: CreateReactSurfaceMountOptions<Props, Action>) => {
return ({
container,
initialProps,
onProps,
onMessage: subscribeMessages,
dispatch,
reportError,
}: BrowserSurfaceMountApi) => {
const root =
surfaceRoots.get(container) ??
(() => {
const createdRoot = createRoot(container);
surfaceRoots.set(container, createdRoot);
return createdRoot;
})();
let currentProps = parseProps(initialProps);
let propsVersion = 0;
const reportSurfaceError = (error: unknown, componentStack?: string) => {
if (!(error instanceof Error)) {
reportError?.(error);
return;
}
if (componentStack && componentStack.trim().length > 0) {
const errorWithComponentStack = new Error(error.message);
errorWithComponentStack.name = error.name;
errorWithComponentStack.stack = [
error.stack ?? `${error.name}: ${error.message}`,
"",
"Component stack:",
componentStack.trim(),
].join("\n");
reportError?.(errorWithComponentStack);
return;
}
reportError?.(error);
};
const render = () => {
root.render(
<SurfaceErrorBoundary
resetToken={propsVersion}
onError={(error: Error, info: ErrorInfo) => {
reportSurfaceError(error, info.componentStack ?? undefined);
}}
>
<Component
props={currentProps}
dispatch={(action: Action) => {
dispatch(normalizeAction(action));
}}
/>
</SurfaceErrorBoundary>,
);
};
render();
const unsubscribeProps = onProps((nextProps) => {
try {
currentProps = parseProps(nextProps);
propsVersion += 1;
render();
} catch (error) {
reportSurfaceError(error);
}
});
const unsubscribeMessages = subscribeMessages((message) => {
onMessage?.(message);
});
return {
unmount: () => {
unsubscribeProps();
unsubscribeMessages();
root.unmount();
if (surfaceRoots.get(container) === root) {
surfaceRoots.delete(container);
}
},
};
};
};
export const createReactBrowserSurfaceMount = <Props, Action>({
Component,
propsSchema,
actionSchema,
onMessage,
}: CreateReactBrowserSurfaceMountOptions<Props, Action>) =>
createReactSurfaceMount({
Component,
parseProps: (value) => propsSchema.parse(value),
normalizeAction: (action) => actionSchema.parse(action),
onMessage,
});
+174
View File
@@ -0,0 +1,174 @@
import type { PackageSchema } from "./index.js";
import z from "zod";
export type JsonValue =
| null
| boolean
| number
| string
| JsonValue[]
| { [key: string]: JsonValue };
export const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.array(jsonValueSchema),
z.record(z.string(), jsonValueSchema),
]),
);
type BrowserSurfaceSchemaLike = PackageSchema & {
__quixos?: {
name?: string | null;
flakeRef?: string | null;
};
};
type BrowserSurfaceAnnotation = {
type: "quixos.ui.surface/v1";
surfaceId: string;
};
const browserSurfaceAnnotationSchema = z.object({
type: z.literal("quixos.ui.surface/v1"),
surfaceId: z.string().min(1),
});
const normalizePackageName = (value: string) => {
const normalized = value.startsWith("@quixos-package-schemas/")
? value.slice("@quixos-package-schemas/".length)
: value;
return normalized.endsWith(".git")
? normalized.slice(0, -".git".length)
: normalized;
};
const packageNameFromFlakeRef = (flakeRef: string) => {
const withoutQuery = flakeRef.split("?")[0] ?? flakeRef;
const withoutGitPrefix = withoutQuery.startsWith("git+")
? withoutQuery.slice(4)
: withoutQuery;
try {
const parsed = new URL(withoutGitPrefix);
const segments = parsed.pathname.split("/").filter(Boolean);
const packageName = segments.at(-1);
if (packageName) {
return normalizePackageName(packageName);
}
} catch {
// Fall through to a simple path split for non-URL flake refs.
}
const segments = withoutGitPrefix.split("/").filter(Boolean);
const packageName = segments.at(-1);
if (!packageName) {
throw new Error(`Unable to determine package name for ${flakeRef}`);
}
return normalizePackageName(packageName);
};
const resolveSchemaPackageName = (schema: BrowserSurfaceSchemaLike) => {
if (typeof schema.__quixos?.name === "string" && schema.__quixos.name.length > 0) {
return normalizePackageName(schema.__quixos.name);
}
if (
typeof schema.__quixos?.flakeRef === "string" &&
schema.__quixos.flakeRef.length > 0
) {
return packageNameFromFlakeRef(schema.__quixos.flakeRef);
}
throw new Error(
"Package schema is missing __quixos metadata. Rebuild the schema with quixos helpers.",
);
};
const findDefaultSurfaceId = (schema: BrowserSurfaceSchemaLike) => {
const annotations = Array.isArray(schema.annotations) ? schema.annotations : [];
for (const annotation of annotations) {
const parsed = browserSurfaceAnnotationSchema.safeParse(annotation);
if (parsed.success) {
return parsed.data.surfaceId;
}
}
return "desktop";
};
const resolvePackageName = (target: string | BrowserSurfaceSchemaLike) =>
typeof target === "string"
? normalizePackageName(target)
: resolveSchemaPackageName(target);
const resolveSurfaceId = (
target: string | BrowserSurfaceSchemaLike,
surfaceId?: string,
) => surfaceId ?? (typeof target === "string" ? "desktop" : findDefaultSurfaceId(target));
export type EmbeddedSurfaceRef<
PackageName extends string = string,
SurfaceId extends string = string,
> = {
packageName: PackageName;
stateContextId: string;
surfaceId: SurfaceId;
};
export const embeddedSurfaceRefSchema = z.object({
packageName: z.string().min(1),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1).default("desktop"),
});
export const createEmbeddedSurfaceRefSchema = <
PackageName extends string = string,
SurfaceId extends string = string,
>(
target: string | BrowserSurfaceSchemaLike,
options?: {
surfaceId?: SurfaceId;
},
) => {
const packageName = resolvePackageName(target) as PackageName;
const surfaceId = resolveSurfaceId(
target,
options?.surfaceId,
) as SurfaceId;
return z.object({
packageName: z.literal(packageName),
stateContextId: z.string().min(1),
surfaceId: z.string().min(1).default(surfaceId),
});
};
export const createEmbeddedSurfaceRef = <
PackageName extends string = string,
SurfaceId extends string = string,
>(
target: string | BrowserSurfaceSchemaLike,
options: {
stateContextId: string;
surfaceId?: SurfaceId;
},
): EmbeddedSurfaceRef<PackageName, SurfaceId> => {
const packageName = resolvePackageName(target) as PackageName;
const surfaceId = resolveSurfaceId(target, options.surfaceId) as SurfaceId;
return {
packageName,
stateContextId: options.stateContextId,
surfaceId,
};
};
export const getEmbeddedSurfacePackageName = (
target: string | BrowserSurfaceSchemaLike,
) => resolvePackageName(target);
export const getEmbeddedSurfaceId = (
target: string | BrowserSurfaceSchemaLike,
surfaceId?: string,
) => resolveSurfaceId(target, surfaceId);
export type { BrowserSurfaceAnnotation };
+1853
View File
File diff suppressed because it is too large Load Diff
+249
View File
@@ -0,0 +1,249 @@
import z from "zod";
export const AuthMessageSchema = z.object({
type: z.literal("auth"),
secret: z.string().min(1),
pid: z.number().int().nonnegative().optional(),
});
export const AuthAckMessageSchema = z.object({
type: z.literal("auth-ack"),
});
export const RpcMethodSchema = z.enum([
"stop",
"call",
"boot",
"target-context-open",
"runtime-error",
"context-open",
"context-close",
"event-subscribe",
"subscription-ready",
"event-unsubscribe",
"value-get",
"value-watch",
"value-unwatch",
"subscription-publish",
]);
export const RequestMessageSchema = z.object({
type: z.literal("request"),
requestId: z.string().min(1),
method: RpcMethodSchema,
params: z.record(z.string(), z.unknown()),
});
const RoutedStateContextFields = {
target: z.string().min(1).optional(),
targetPackageName: z.string().min(1).optional(),
callerStateContextId: z.string().min(1).optional(),
stateContextId: z.string().min(1).optional(),
contextNamespace: z.string().min(1).optional(),
};
export const CallRequestParamsSchema = z.object({
...RoutedStateContextFields,
functionName: z.string().min(1),
params: z.unknown(),
});
export const BootRequestParamsSchema = z.object({
target: z.string().min(1),
});
export const TargetContextOpenRequestParamsSchema = z.object({
...RoutedStateContextFields,
target: z.string().min(1),
targetPackageName: z.string().min(1).optional(),
});
export const RuntimeErrorRequestParamsSchema = z.object({
phase: z.enum([
"on-create",
"on-destroy",
"on-context-open",
"on-context-close",
"browser-surface",
"function",
"event-subscribe",
"event-cleanup",
"value-get",
"value-watch",
"value-cleanup",
"subscription-publish",
"internal",
]),
stateContextId: z.string().min(1).optional(),
functionName: z.string().min(1).optional(),
resourceKind: z.enum(["event", "value"]).optional(),
resourceName: z.string().min(1).optional(),
surfaceId: z.string().min(1).optional(),
hostSessionId: z.string().min(1).optional(),
message: z.string().min(1),
stack: z.string().min(1).optional(),
});
export const ContextOpenRequestParamsSchema = z.object({
stateContextId: z.string().min(1),
});
export const ContextCloseRequestParamsSchema = z.object({
stateContextId: z.string().min(1),
});
export const EventSubscribeRequestParamsSchema = z.object({
...RoutedStateContextFields,
eventName: z.string().min(1),
params: z.unknown().optional(),
subscriptionNamespace: z.string().min(1).optional(),
});
export const EventUnsubscribeRequestParamsSchema = z.object({
subscriptionId: z.string().min(1),
});
export const SubscriptionReadyRequestParamsSchema = z.object({
subscriptionId: z.string().min(1),
});
export const ValueGetRequestParamsSchema = z.object({
...RoutedStateContextFields,
valueName: z.string().min(1),
params: z.unknown().optional(),
});
export const ValueWatchRequestParamsSchema = z.object({
...RoutedStateContextFields,
valueName: z.string().min(1),
params: z.unknown().optional(),
subscriptionNamespace: z.string().min(1).optional(),
});
export const ValueUnwatchRequestParamsSchema = z.object({
subscriptionId: z.string().min(1),
});
export const SubscriptionPublishRequestParamsSchema = z.object({
subscriptionId: z.string().min(1),
data: z.unknown(),
});
export const SubscriptionResponseSchema = z.object({
subscriptionId: z.string().min(1),
});
export const SubscriptionDataMessageSchema = z.object({
type: z.literal("subscription-data"),
subscriptionId: z.string().min(1),
data: z.unknown(),
});
export const ResponseOkMessageSchema = z.object({
type: z.literal("response"),
requestId: z.string().min(1),
ok: z.literal(true),
result: z.unknown().optional(),
});
export const ResponseErrorMessageSchema = z.object({
type: z.literal("response"),
requestId: z.string().min(1),
ok: z.literal(false),
error: z.string(),
});
export const ResponseMessageSchema = z.union([
ResponseOkMessageSchema,
ResponseErrorMessageSchema,
]);
export const ServerMessageSchema = z.union([
AuthAckMessageSchema,
RequestMessageSchema,
ResponseMessageSchema,
SubscriptionDataMessageSchema,
]);
export const ClientMessageSchema = z.union([
AuthMessageSchema,
ResponseMessageSchema,
RequestMessageSchema,
]);
export type AuthMessage = z.infer<typeof AuthMessageSchema>;
export type AuthAckMessage = z.infer<typeof AuthAckMessageSchema>;
export type RequestMessage = z.infer<typeof RequestMessageSchema>;
export type CallRequestParams = z.infer<typeof CallRequestParamsSchema>;
export type BootRequestParams = z.infer<typeof BootRequestParamsSchema>;
export type TargetContextOpenRequestParams = z.infer<
typeof TargetContextOpenRequestParamsSchema
>;
export type RuntimeErrorRequestParams = z.infer<
typeof RuntimeErrorRequestParamsSchema
>;
export type ContextOpenRequestParams = z.infer<
typeof ContextOpenRequestParamsSchema
>;
export type ContextCloseRequestParams = z.infer<
typeof ContextCloseRequestParamsSchema
>;
export type EventSubscribeRequestParams = z.infer<
typeof EventSubscribeRequestParamsSchema
>;
export type EventUnsubscribeRequestParams = z.infer<
typeof EventUnsubscribeRequestParamsSchema
>;
export type SubscriptionReadyRequestParams = z.infer<
typeof SubscriptionReadyRequestParamsSchema
>;
export type ValueGetRequestParams = z.infer<typeof ValueGetRequestParamsSchema>;
export type ValueWatchRequestParams = z.infer<
typeof ValueWatchRequestParamsSchema
>;
export type ValueUnwatchRequestParams = z.infer<
typeof ValueUnwatchRequestParamsSchema
>;
export type SubscriptionPublishRequestParams = z.infer<
typeof SubscriptionPublishRequestParamsSchema
>;
export type SubscriptionResponse = z.infer<typeof SubscriptionResponseSchema>;
export type SubscriptionDataMessage = z.infer<
typeof SubscriptionDataMessageSchema
>;
export type ResponseMessage = z.infer<typeof ResponseMessageSchema>;
export type ClientMessage = z.infer<typeof ClientMessageSchema>;
export type ServerMessage = z.infer<typeof ServerMessageSchema>;
type SocketLike = {
send: (data: string) => void;
};
export const sendMessage = <Message extends ClientMessage | ServerMessage>(
socket: SocketLike,
message: Message,
) => {
socket.send(JSON.stringify(message));
};
export const sendClientMessage = (
socket: SocketLike,
message: ClientMessage,
) => {
sendMessage(socket, message);
};
export const sendServerMessage = (
socket: SocketLike,
message: ServerMessage,
) => {
sendMessage(socket, message);
};
export const parseJson = (value: string) => {
try {
return { ok: true as const, value: JSON.parse(value) };
} catch {
return { ok: false as const };
}
};
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022", "DOM"],
"strict": true,
"outDir": "dist",
"rootDir": ".",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": true,
"types": ["node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "quixos-package-schema.ts"],
"exclude": ["node_modules", ".yarn"]
}
+106
View File
@@ -0,0 +1,106 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@*":
version "25.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.9.1.tgz#3bda556db500ae4319c08e7fc9ab94f19013ba0b"
integrity sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==
dependencies:
undici-types ">=7.24.0 <7.24.7"
"@types/node@^24":
version "24.12.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.12.4.tgz#2709745569811dcbdc57c097fafdd387c6330382"
integrity sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==
dependencies:
undici-types "~7.16.0"
"@types/prop-types@*":
version "15.7.15"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
"@types/react-dom@^18.3.1":
version "18.3.7"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f"
integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==
"@types/react@^18.3.12":
version "18.3.29"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.29.tgz#7f3b6e1515499d4fd199cc8fd4710114be36c1a2"
integrity sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==
dependencies:
"@types/prop-types" "*"
csstype "^3.2.2"
"@types/ws@^8.18.1":
version "8.18.1"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9"
integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==
dependencies:
"@types/node" "*"
csstype@^3.2.2:
version "3.2.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
react-dom@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.2"
react@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
dependencies:
loose-envify "^1.1.0"
scheduler@^0.23.2:
version "0.23.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
dependencies:
loose-envify "^1.1.0"
typescript@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
"undici-types@>=7.24.0 <7.24.7":
version "7.24.6"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.24.6.tgz#61275b485d7fd4e9d269c7cf04ec2873c9cc0f91"
integrity sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==
undici-types@~7.16.0:
version "7.16.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
ws@^8.18.3:
version "8.21.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951"
integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==
zod@^4.3.6:
version "4.4.3"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356"
integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==