Initial commit
germanium cutoff
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,4 @@
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
||||
+19
@@ -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
|
||||
Vendored
+122
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+403
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+33
@@ -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
@@ -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"}
|
||||
Vendored
+444
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
Vendored
+34
@@ -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
@@ -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"}
|
||||
Vendored
+94
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"browserSurfaceReact.js","sourceRoot":"","sources":["../../src/browserSurfaceReact.tsx"],"names":[],"mappings":"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"}
|
||||
Vendored
+41
@@ -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
@@ -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"}
|
||||
Vendored
+94
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"browserSurfaceShared.js","sourceRoot":"","sources":["../../src/browserSurfaceShared.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+178
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1154
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+280
@@ -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
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../src/rpc.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+165
@@ -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
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+249
@@ -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 };
|
||||
}
|
||||
};
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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==
|
||||
Reference in New Issue
Block a user