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