182 lines
5.1 KiB
TypeScript
182 lines
5.1 KiB
TypeScript
/**
|
|
* Layer 5: Compute — Deterministic transforms that run locally when possible,
|
|
* delegate to server when not.
|
|
*
|
|
* | Transform | Local? | Example |
|
|
* |---------------------|--------|--------------------------------|
|
|
* | Markdown → HTML | Yes | Notes rendering |
|
|
* | Search indexing | Yes | Build index from docs |
|
|
* | Vote tallies | Yes | Derived from CRDT state |
|
|
* | PDF generation | No | Server delegate (Typst) |
|
|
* | Image thumbnailing | No | Server delegate (Sharp) |
|
|
*
|
|
* Server fallback: POST /:space/api/compute/:transformId
|
|
*/
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
/**
|
|
* A transform: a named, deterministic function that converts input → output.
|
|
*/
|
|
export interface Transform<In = unknown, Out = unknown> {
|
|
/** Unique identifier (e.g. "markdown-to-html", "pdf-generate") */
|
|
id: string;
|
|
/** Whether this transform can run in the browser */
|
|
canRunLocally: boolean;
|
|
/** Execute the transform */
|
|
execute(input: In): Promise<Out>;
|
|
}
|
|
|
|
export interface ComputeEngineOptions {
|
|
/** Base URL for server-side compute endpoint (e.g. "/demo/api/compute") */
|
|
serverBaseUrl?: string;
|
|
/** Auth token for server requests */
|
|
authToken?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// ComputeEngine
|
|
// ============================================================================
|
|
|
|
export class ComputeEngine {
|
|
#transforms = new Map<string, Transform<any, any>>();
|
|
#serverBaseUrl: string | null;
|
|
#authToken: string | null;
|
|
|
|
constructor(opts: ComputeEngineOptions = {}) {
|
|
this.#serverBaseUrl = opts.serverBaseUrl ?? null;
|
|
this.#authToken = opts.authToken ?? null;
|
|
}
|
|
|
|
/**
|
|
* Register a transform.
|
|
*/
|
|
register<In, Out>(transform: Transform<In, Out>): void {
|
|
this.#transforms.set(transform.id, transform);
|
|
}
|
|
|
|
/**
|
|
* Run a transform. Runs locally if possible, delegates to server otherwise.
|
|
*/
|
|
async run<In, Out>(transformId: string, input: In): Promise<Out> {
|
|
const transform = this.#transforms.get(transformId);
|
|
|
|
if (transform?.canRunLocally) {
|
|
return transform.execute(input) as Promise<Out>;
|
|
}
|
|
|
|
if (transform && !transform.canRunLocally && this.#serverBaseUrl) {
|
|
return this.#delegateToServer<In, Out>(transformId, input);
|
|
}
|
|
|
|
if (!transform && this.#serverBaseUrl) {
|
|
// Transform not registered locally — try server
|
|
return this.#delegateToServer<In, Out>(transformId, input);
|
|
}
|
|
|
|
throw new Error(
|
|
`Transform "${transformId}" not available: ${
|
|
transform ? 'requires server but no serverBaseUrl configured' : 'not registered'
|
|
}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a transform is registered and can run locally.
|
|
*/
|
|
canRunLocally(transformId: string): boolean {
|
|
const t = this.#transforms.get(transformId);
|
|
return !!t && t.canRunLocally;
|
|
}
|
|
|
|
/**
|
|
* Check if a transform is registered.
|
|
*/
|
|
has(transformId: string): boolean {
|
|
return this.#transforms.has(transformId);
|
|
}
|
|
|
|
/**
|
|
* List all registered transform IDs.
|
|
*/
|
|
list(): string[] {
|
|
return Array.from(this.#transforms.keys());
|
|
}
|
|
|
|
/**
|
|
* Update auth token (e.g. after login/refresh).
|
|
*/
|
|
setAuthToken(token: string): void {
|
|
this.#authToken = token;
|
|
}
|
|
|
|
/**
|
|
* Update server base URL.
|
|
*/
|
|
setServerBaseUrl(url: string): void {
|
|
this.#serverBaseUrl = url;
|
|
}
|
|
|
|
// ---------- Private ----------
|
|
|
|
async #delegateToServer<In, Out>(transformId: string, input: In): Promise<Out> {
|
|
if (!this.#serverBaseUrl) {
|
|
throw new Error('No server base URL configured for compute delegation');
|
|
}
|
|
|
|
const url = `${this.#serverBaseUrl}/${transformId}`;
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
if (this.#authToken) {
|
|
headers['Authorization'] = `Bearer ${this.#authToken}`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({ input }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Compute server error: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
return result.output as Out;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// BUILT-IN TRANSFORMS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Helper to create a local transform.
|
|
*/
|
|
export function localTransform<In, Out>(
|
|
id: string,
|
|
fn: (input: In) => Promise<Out> | Out
|
|
): Transform<In, Out> {
|
|
return {
|
|
id,
|
|
canRunLocally: true,
|
|
execute: async (input) => fn(input),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Helper to declare a server-only transform (acts as a type contract).
|
|
*/
|
|
export function serverTransform<In, Out>(id: string): Transform<In, Out> {
|
|
return {
|
|
id,
|
|
canRunLocally: false,
|
|
execute: async () => {
|
|
throw new Error(`Transform "${id}" must run on server`);
|
|
},
|
|
};
|
|
}
|