This commit is contained in:
parent
9d4d67f086
commit
0c9a9269c4
@ -131,40 +131,99 @@ export function humanizeDuration(durationInMilliseconds: number) {
|
|||||||
|
|
||||||
return durationString.trim();
|
return durationString.trim();
|
||||||
}
|
}
|
||||||
export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> {
|
// export function debounceAsyncFunction<T extends any[], R>(
|
||||||
let currentPromise: Promise<R> | null = null;
|
// func: (...args: T) => Promise<R>
|
||||||
let nextArgs: T | null = null;
|
// ): (...args: T) => Promise<R> {
|
||||||
let resolveNext: ((result: R) => void) | null = null;
|
// let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
// let lastPromise: Promise<R> | null = null;
|
||||||
|
// let lastReject: ((reason?: any) => void) | null = null;
|
||||||
|
//
|
||||||
|
// return (...args: T): Promise<R> => {
|
||||||
|
// if (timeoutId) {
|
||||||
|
// clearTimeout(timeoutId);
|
||||||
|
// if (lastReject) {
|
||||||
|
// lastReject(new Error("Debounced: Previous call was canceled."));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return new Promise<R>((resolve, reject) => {
|
||||||
|
// lastReject = reject;
|
||||||
|
// timeoutId = setTimeout(() => {
|
||||||
|
// timeoutId = null;
|
||||||
|
// lastReject = null;
|
||||||
|
// lastPromise = func(...args).then(resolve, reject);
|
||||||
|
// }, 300); // Default debounce time is 300ms; you can make this configurable.
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
export function debounceAsyncFunction<T extends (...args: any[]) => Promise<any>>(asyncFn: T): T {
|
||||||
|
let isRunning = false;
|
||||||
|
let latestArgs: Parameters<T> | null = null;
|
||||||
|
let resolveNext: (() => void) | null = null;
|
||||||
|
|
||||||
const debouncedFunction = async (...args: T): Promise<R> => {
|
return (async function serializedFunction(...args: Parameters<T>): Promise<ReturnType<T>> {
|
||||||
if (currentPromise) {
|
latestArgs = args;
|
||||||
// Store the latest arguments and create a new promise to resolve them later
|
|
||||||
nextArgs = args;
|
if (isRunning) {
|
||||||
return new Promise<R>((resolve) => {
|
// Wait for the current execution to finish
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
resolveNext = resolve;
|
resolveNext = resolve;
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
// Execute the function immediately
|
|
||||||
|
// Indicate the function is running
|
||||||
|
isRunning = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
currentPromise = func(...args);
|
// Execute with the latest arguments
|
||||||
const result = await currentPromise;
|
const result = await asyncFn(...latestArgs!);
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
currentPromise = null;
|
// Allow the next execution
|
||||||
// If there are stored arguments, call the function again with the latest arguments
|
isRunning = false;
|
||||||
if (nextArgs) {
|
|
||||||
const argsToUse = nextArgs;
|
|
||||||
const resolver = resolveNext;
|
|
||||||
nextArgs = null;
|
|
||||||
resolveNext = null;
|
|
||||||
resolver!(await debouncedFunction(...argsToUse));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return debouncedFunction;
|
if (resolveNext) {
|
||||||
|
resolveNext();
|
||||||
|
resolveNext = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function debounceAsyncFunction<T extends any[], R>(func: (...args: T) => Promise<R>): (...args: T) => Promise<R> {
|
||||||
|
// let currentPromise: Promise<R> | null = null;
|
||||||
|
// let nextArgs: T | null = null;
|
||||||
|
// let resolveNext: ((result: R) => void) | null = null;
|
||||||
|
//
|
||||||
|
// const debouncedFunction = async (...args: T): Promise<R> => {
|
||||||
|
// if (currentPromise) {
|
||||||
|
// // Store the latest arguments and create a new promise to resolve them later
|
||||||
|
// nextArgs = args;
|
||||||
|
// return new Promise<R>((resolve) => {
|
||||||
|
// resolveNext = resolve;
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// // Execute the function immediately
|
||||||
|
// try {
|
||||||
|
// currentPromise = func(...args);
|
||||||
|
// const result = await currentPromise;
|
||||||
|
// return result;
|
||||||
|
// } finally {
|
||||||
|
// currentPromise = null;
|
||||||
|
// // If there are stored arguments, call the function again with the latest arguments
|
||||||
|
// if (nextArgs) {
|
||||||
|
// const argsToUse = nextArgs;
|
||||||
|
// const resolver = resolveNext;
|
||||||
|
// nextArgs = null;
|
||||||
|
// resolveNext = null;
|
||||||
|
// resolver!(await debouncedFunction(...argsToUse));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// return debouncedFunction;
|
||||||
|
// }
|
||||||
|
|
||||||
export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R): (...args: T) => R {
|
export function withArgsChangeOnly<T extends any[], R>(func: (...args: T) => R): (...args: T) => R {
|
||||||
let lastArgs: T | undefined = undefined;
|
let lastArgs: T | undefined = undefined;
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
let performanceStore = createPerformanceStore();
|
let performanceStore = createPerformanceStore();
|
||||||
|
|
||||||
const registryCache = new IndexDBCache("node-registry");
|
const registryCache = new IndexDBCache("node-registry");
|
||||||
const nodeRegistry = new RemoteNodeRegistry("");
|
const nodeRegistry = new RemoteNodeRegistry("http://localhost:8000/v1");
|
||||||
nodeRegistry.cache = registryCache;
|
nodeRegistry.cache = registryCache;
|
||||||
const workerRuntime = new WorkerRuntimeExecutor();
|
const workerRuntime = new WorkerRuntimeExecutor();
|
||||||
const runtimeCache = new MemoryRuntimeCache();
|
const runtimeCache = new MemoryRuntimeCache();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { type NodeRegistry, type NodeDefinition, NodeDefinitionSchema, type AsyncCache } from "@nodes/types";
|
import { NodeDefinitionSchema, type AsyncCache, type NodeDefinition, type NodeRegistry } from "@nodes/types";
|
||||||
import { createWasmWrapper, createLogger } from "@nodes/utils";
|
import { createLogger, createWasmWrapper } from "@nodes/utils";
|
||||||
|
|
||||||
const log = createLogger("node-registry");
|
const log = createLogger("node-registry");
|
||||||
log.mute();
|
log.mute();
|
||||||
@ -49,18 +49,21 @@ export class RemoteNodeRegistry implements NodeRegistry {
|
|||||||
|
|
||||||
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
|
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
|
||||||
|
|
||||||
|
const fetchNode = async () => {
|
||||||
const response = await this.fetch(`${this.url}/nodes/${nodeId}.wasm`);
|
const response = await this.fetch(`${this.url}/nodes/${nodeId}.wasm`);
|
||||||
if (!response.ok) {
|
return response.arrayBuffer();
|
||||||
if (this.cache) {
|
|
||||||
let value = await this.cache.get(nodeId);
|
|
||||||
if (value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const res = await Promise.race([
|
||||||
|
fetchNode(),
|
||||||
|
this.cache?.get(nodeId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
throw new Error(`Failed to load node wasm ${nodeId}`);
|
throw new Error(`Failed to load node wasm ${nodeId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.arrayBuffer();
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(nodeIds: `${string}/${string}/${string}`[]) {
|
async load(nodeIds: `${string}/${string}/${string}`[]) {
|
||||||
|
@ -52,8 +52,10 @@ async function postNode(node: Node) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const json = await res.text();
|
console.log(`Uploaded ${node.id}`);
|
||||||
console.log(json);
|
} else {
|
||||||
|
const text = await res.text();
|
||||||
|
console.log(`Failed to upload ${node.id}: ${text}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --watch main.ts",
|
"dev": "deno run --watch main.ts",
|
||||||
"test": "deno run vitest",
|
"test": "deno run vitest",
|
||||||
"drizzle": "docker compose exec app deno --env -A --node-modules-dir npm:drizzle-kit",
|
"drizzle": "podman-compose exec app deno --env -A --node-modules-dir npm:drizzle-kit",
|
||||||
"upload": "deno run --allow-read --allow-net bin/upload.ts"
|
"upload": "deno run --allow-read --allow-net bin/upload.ts"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export * from "../routes/user/user.schema.ts";
|
export * from "../routes/user/user.schema.ts";
|
||||||
export * from "../routes/node/node.schema.ts";
|
export * from "../routes/node/schemas/node.schema.ts";
|
||||||
|
@ -12,14 +12,14 @@ app.use("/v1/*", cors());
|
|||||||
app.use(logger());
|
app.use(logger());
|
||||||
app.route("v1", router);
|
app.route("v1", router);
|
||||||
|
|
||||||
app.doc("/doc", {
|
app.doc("/openapi.json", {
|
||||||
openapi: "3.0.0",
|
openapi: "3.0.0",
|
||||||
info: {
|
info: {
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
title: "My API",
|
title: "Nodarium API",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/ui", swaggerUI({ url: "/doc" }));
|
app.get("/ui", swaggerUI({ url: "/openapi.json" }));
|
||||||
|
|
||||||
Deno.serve(app.fetch);
|
Deno.serve(app.fetch);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
||||||
import { NodeDefinitionSchema } from "./types.ts";
|
import { HTTPException } from "hono/http-exception";
|
||||||
|
import { idRegex, NodeDefinitionSchema } from "./schemas/types.ts";
|
||||||
import * as service from "./node.service.ts";
|
import * as service from "./node.service.ts";
|
||||||
import { bodyLimit } from "hono/body-limit";
|
import { bodyLimit } from "hono/body-limit";
|
||||||
|
|
||||||
@ -11,24 +12,49 @@ const SingleParam = (name: string) =>
|
|||||||
.min(3)
|
.min(3)
|
||||||
.max(20)
|
.max(20)
|
||||||
.refine(
|
.refine(
|
||||||
(value) => /^[a-z_-]+$/i.test(value),
|
(value) => idRegex.test(value),
|
||||||
"Name should contain only alphabets",
|
"Name should contain only alphabets",
|
||||||
)
|
)
|
||||||
.openapi({ param: { name, in: "path" } });
|
.openapi({ param: { name, in: "path" } });
|
||||||
|
|
||||||
const ParamsSchema = z.object({
|
const ParamsSchema = z.object({
|
||||||
userId: SingleParam("userId"),
|
user: SingleParam("user"),
|
||||||
nodeSystemId: SingleParam("nodeSystemId"),
|
system: SingleParam("system"),
|
||||||
nodeId: SingleParam("nodeId"),
|
nodeId: SingleParam("nodeId"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getUserNodesRoute = createRoute({
|
||||||
|
method: "get",
|
||||||
|
path: "/{user}.json",
|
||||||
|
request: {
|
||||||
|
params: z.object({
|
||||||
|
user: SingleParam("user"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: z.array(NodeDefinitionSchema),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "Retrieve a single node definition",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
nodeRouter.openapi(getUserNodesRoute, async (c) => {
|
||||||
|
const userId = c.req.param("user.json").replace(/\.json$/, "");
|
||||||
|
const nodes = await service.getNodeDefinitionsByUser(userId);
|
||||||
|
return c.json(nodes);
|
||||||
|
});
|
||||||
|
|
||||||
const getNodeCollectionRoute = createRoute({
|
const getNodeCollectionRoute = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/{userId}/{nodeSystemId}.json",
|
path: "/{user}/{system}.json",
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
userId: SingleParam("userId"),
|
user: SingleParam("user"),
|
||||||
nodeSystemId: SingleParam("nodeSystemId").optional(),
|
system: SingleParam("system").optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
@ -43,17 +69,16 @@ const getNodeCollectionRoute = createRoute({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
nodeRouter.openapi(getNodeCollectionRoute, async (c) => {
|
nodeRouter.openapi(getNodeCollectionRoute, async (c) => {
|
||||||
const { userId } = c.req.valid("param");
|
const { user } = c.req.valid("param");
|
||||||
const nodeSystemId = c.req.param("nodeSystemId.json").replace(/\.json$/, "");
|
const nodeSystemId = c.req.param("system.json").replace(/\.json$/, "");
|
||||||
|
|
||||||
const nodes = await service.getNodesBySystem(userId, nodeSystemId);
|
|
||||||
|
|
||||||
|
const nodes = await service.getNodesBySystem(user, nodeSystemId);
|
||||||
return c.json(nodes);
|
return c.json(nodes);
|
||||||
});
|
});
|
||||||
|
|
||||||
const getNodeDefinitionRoute = createRoute({
|
const getNodeDefinitionRoute = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/{userId}/{nodeSystemId}/{nodeId}.json",
|
path: "/{user}/{system}/{nodeId}{.+\\.json}",
|
||||||
request: {
|
request: {
|
||||||
params: ParamsSchema,
|
params: ParamsSchema,
|
||||||
},
|
},
|
||||||
@ -68,15 +93,25 @@ const getNodeDefinitionRoute = createRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
nodeRouter.openapi(getNodeDefinitionRoute, (c) => {
|
nodeRouter.openapi(getNodeDefinitionRoute, async (c) => {
|
||||||
return c.json({
|
const { user, system, nodeId } = c.req.valid("param");
|
||||||
id: "",
|
|
||||||
});
|
const node = await service.getNodeDefinitionById(
|
||||||
|
user,
|
||||||
|
system,
|
||||||
|
nodeId.replace(/\.json$/, ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
throw new HTTPException(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
const getNodeWasmRoute = createRoute({
|
const getNodeWasmRoute = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/{userId}/{nodeSystemId}/{nodeId}.wasm",
|
path: "/{user}/{system}/{nodeId}{.+\\.wasm}",
|
||||||
request: {
|
request: {
|
||||||
params: ParamsSchema,
|
params: ParamsSchema,
|
||||||
},
|
},
|
||||||
@ -91,11 +126,18 @@ const getNodeWasmRoute = createRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
nodeRouter.openapi(getNodeWasmRoute, async (c) => {
|
||||||
|
const { user, system, nodeId } = c.req.valid("param");
|
||||||
|
|
||||||
nodeRouter.openapi(getNodeWasmRoute, (c) => {
|
const wasmContent = await service.getNodeWasmById(
|
||||||
return c.json({
|
user,
|
||||||
id: "",
|
system,
|
||||||
});
|
nodeId.replace(/\.wasm/, ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
c.header("Content-Type", "application/wasm");
|
||||||
|
|
||||||
|
return c.body(wasmContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
const createNodeRoute = createRoute({
|
const createNodeRoute = createRoute({
|
||||||
@ -113,20 +155,17 @@ const createNodeRoute = createRoute({
|
|||||||
},
|
},
|
||||||
middleware: [
|
middleware: [
|
||||||
bodyLimit({
|
bodyLimit({
|
||||||
maxSize: 50 * 1024, // 50kb
|
maxSize: 128 * 1024, // 128kb
|
||||||
onError: (c) => {
|
onError: (c) => {
|
||||||
return c.text("overflow :(", 413);
|
return c.text("Node content too large", 413);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
nodeRouter.openapi(createNodeRoute, async (c) => {
|
nodeRouter.openapi(createNodeRoute, async (c) => {
|
||||||
const buffer = await c.req.arrayBuffer();
|
const buffer = await c.req.arrayBuffer();
|
||||||
const bytes = await (await c.req.blob()).bytes();
|
const bytes = await (await c.req.blob()).bytes();
|
||||||
|
|
||||||
const node = await service.createNode(buffer, bytes);
|
const node = await service.createNode(buffer, bytes);
|
||||||
|
|
||||||
return c.json(node);
|
return c.json(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { db } from "../../db/db.ts";
|
import { db } from "../../db/db.ts";
|
||||||
import { nodeTable } from "./node.schema.ts";
|
import { nodeTable } from "./schemas/node.schema.ts";
|
||||||
import { NodeDefinition, NodeDefinitionSchema } from "./types.ts";
|
import { NodeDefinition, NodeDefinitionSchema } from "./schemas/types.ts";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { WorkerMessage } from "./worker/types.ts";
|
||||||
|
|
||||||
export type CreateNodeDTO = {
|
export type CreateNodeDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -10,10 +12,21 @@ export type CreateNodeDTO = {
|
|||||||
content: ArrayBuffer;
|
content: ArrayBuffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getNodeHash(content: Uint8Array) {
|
||||||
|
const hash = createHash("sha256");
|
||||||
|
hash.update(content);
|
||||||
|
return hash.digest("hex").slice(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
function extractDefinition(content: ArrayBuffer): Promise<NodeDefinition> {
|
function extractDefinition(content: ArrayBuffer): Promise<NodeDefinition> {
|
||||||
const worker = new Worker(new URL("./node.worker.ts", import.meta.url).href, {
|
const worker = new Worker(
|
||||||
|
new URL("./worker/node.worker.ts", import.meta.url).href,
|
||||||
|
{
|
||||||
type: "module",
|
type: "module",
|
||||||
});
|
},
|
||||||
|
) as Worker & {
|
||||||
|
postMessage: (message: WorkerMessage) => void;
|
||||||
|
};
|
||||||
|
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
worker.postMessage({ action: "extract-definition", content });
|
worker.postMessage({ action: "extract-definition", content });
|
||||||
@ -22,12 +35,12 @@ function extractDefinition(content: ArrayBuffer): Promise<NodeDefinition> {
|
|||||||
rej(new Error("Worker timeout out"));
|
rej(new Error("Worker timeout out"));
|
||||||
}, 100);
|
}, 100);
|
||||||
worker.onmessage = function (e) {
|
worker.onmessage = function (e) {
|
||||||
console.log(e.data);
|
|
||||||
switch (e.data.action) {
|
switch (e.data.action) {
|
||||||
case "result":
|
case "result":
|
||||||
res(e.data.result);
|
res(e.data.result);
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
|
console.log("Worker error", e.data.error);
|
||||||
rej(e.data.result);
|
rej(e.data.result);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -51,20 +64,28 @@ export async function createNode(
|
|||||||
systemId,
|
systemId,
|
||||||
nodeId,
|
nodeId,
|
||||||
definition: def,
|
definition: def,
|
||||||
|
hash: getNodeHash(content),
|
||||||
content: content,
|
content: content,
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.insert(nodeTable).values(node);
|
await db.insert(nodeTable).values(node);
|
||||||
console.log("New user created!");
|
console.log("new node created!");
|
||||||
// await db.insert(users).values({ name: "Andrew" });
|
|
||||||
return def;
|
return def;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log({ error });
|
console.log("Creation Error", { error });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodesByUser(userName: string) {}
|
export function getNodeDefinitionsByUser(userName: string) {
|
||||||
|
return db.select({ definition: nodeTable.definition }).from(nodeTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(nodeTable.userId, userName),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getNodesBySystem(
|
export async function getNodesBySystem(
|
||||||
username: string,
|
username: string,
|
||||||
systemId: string,
|
systemId: string,
|
||||||
@ -84,4 +105,51 @@ export async function getNodesBySystem(
|
|||||||
return definitions;
|
return definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodeById(dto: CreateNodeDTO) {}
|
export async function getNodeWasmById(
|
||||||
|
userName: string,
|
||||||
|
systemId: string,
|
||||||
|
nodeId: string,
|
||||||
|
) {
|
||||||
|
const node = await db.select({ content: nodeTable.content }).from(nodeTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(nodeTable.userId, userName),
|
||||||
|
eq(nodeTable.systemId, systemId),
|
||||||
|
eq(nodeTable.nodeId, nodeId),
|
||||||
|
),
|
||||||
|
).limit(1);
|
||||||
|
|
||||||
|
if (!node[0]) {
|
||||||
|
throw new Error("Node not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return node[0].content;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNodeDefinitionById(
|
||||||
|
userName: string,
|
||||||
|
systemId: string,
|
||||||
|
nodeId: string,
|
||||||
|
) {
|
||||||
|
const node = await db.select({ definition: nodeTable.definition }).from(
|
||||||
|
nodeTable,
|
||||||
|
).where(
|
||||||
|
and(
|
||||||
|
eq(nodeTable.userId, userName),
|
||||||
|
eq(nodeTable.systemId, systemId),
|
||||||
|
eq(nodeTable.nodeId, nodeId),
|
||||||
|
),
|
||||||
|
).limit(1);
|
||||||
|
|
||||||
|
if (!node[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = NodeDefinitionSchema.safeParse(node[0]?.definition);
|
||||||
|
|
||||||
|
if (!definition.data) {
|
||||||
|
throw new Error("Invalid definition");
|
||||||
|
}
|
||||||
|
|
||||||
|
return definition.data;
|
||||||
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { router } from "../router.ts";
|
|||||||
Deno.test("simple test", async () => {
|
Deno.test("simple test", async () => {
|
||||||
const res = await router.request("/max/plants/test.json");
|
const res = await router.request("/max/plants/test.json");
|
||||||
const json = await res.text();
|
const json = await res.text();
|
||||||
console.log({ json });
|
|
||||||
|
|
||||||
expect(true).toEqual(true);
|
expect(true).toEqual(true);
|
||||||
|
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
/// <reference lib="webworker" />
|
|
||||||
|
|
||||||
import { NodeDefinitionSchema } from "./types.ts";
|
|
||||||
import { createWasmWrapper } from "./utils.ts";
|
|
||||||
|
|
||||||
function extractDefinition(wasmCode: ArrayBuffer) {
|
|
||||||
const wasm = createWasmWrapper(wasmCode);
|
|
||||||
|
|
||||||
const definition = wasm.get_definition();
|
|
||||||
|
|
||||||
const p = NodeDefinitionSchema.safeParse(definition);
|
|
||||||
|
|
||||||
if (!p.success) {
|
|
||||||
self.postMessage({ action: "error", error: p.error });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.postMessage({ action: "result", result: p.data });
|
|
||||||
}
|
|
||||||
|
|
||||||
self.onmessage = (e) => {
|
|
||||||
switch (e.data.action) {
|
|
||||||
case "extract-definition":
|
|
||||||
extractDefinition(e.data.content);
|
|
||||||
self.close();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error("Unknwon action", e.data.action);
|
|
||||||
}
|
|
||||||
};
|
|
@ -10,7 +10,7 @@ const DefaultOptionsSchema = z.object({
|
|||||||
hidden: z.boolean().optional(),
|
hidden: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeInputFloatSchema = z.object({
|
const NodeInputFloatSchema = z.object({
|
||||||
...DefaultOptionsSchema.shape,
|
...DefaultOptionsSchema.shape,
|
||||||
type: z.literal("float"),
|
type: z.literal("float"),
|
||||||
element: z.literal("slider").optional(),
|
element: z.literal("slider").optional(),
|
||||||
@ -20,7 +20,7 @@ export const NodeInputFloatSchema = z.object({
|
|||||||
step: z.number().optional(),
|
step: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeInputIntegerSchema = z.object({
|
const NodeInputIntegerSchema = z.object({
|
||||||
...DefaultOptionsSchema.shape,
|
...DefaultOptionsSchema.shape,
|
||||||
type: z.literal("integer"),
|
type: z.literal("integer"),
|
||||||
element: z.literal("slider").optional(),
|
element: z.literal("slider").optional(),
|
||||||
@ -29,37 +29,37 @@ export const NodeInputIntegerSchema = z.object({
|
|||||||
max: z.number().optional(),
|
max: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeInputBooleanSchema = z.object({
|
const NodeInputBooleanSchema = z.object({
|
||||||
...DefaultOptionsSchema.shape,
|
...DefaultOptionsSchema.shape,
|
||||||
type: z.literal("boolean"),
|
type: z.literal("boolean"),
|
||||||
value: z.boolean().optional(),
|
value: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeInputSelectSchema = z.object({
|
const NodeInputSelectSchema = z.object({
|
||||||
...DefaultOptionsSchema.shape,
|
...DefaultOptionsSchema.shape,
|
||||||
type: z.literal("select"),
|
type: z.literal("select"),
|
||||||
options: z.array(z.string()).optional(),
|
options: z.array(z.string()).optional(),
|
||||||
value: z.number().optional(),
|
value: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeInputSeedSchema = z.object({
|
const NodeInputSeedSchema = z.object({
|
||||||
...DefaultOptionsSchema.shape,
|
...DefaultOptionsSchema.shape,
|
||||||
type: z.literal("seed"),
|
type: z.literal("seed"),
|
||||||
value: z.number().optional(),
|
value: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeInputVec3Schema = z.object({
|
const NodeInputVec3Schema = z.object({
|
||||||
...DefaultOptionsSchema.shape,
|
...DefaultOptionsSchema.shape,
|
||||||
type: z.literal("vec3"),
|
type: z.literal("vec3"),
|
||||||
value: z.array(z.number()).optional(),
|
value: z.array(z.number()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeInputGeometrySchema = z.object({
|
const NodeInputGeometrySchema = z.object({
|
||||||
...DefaultOptionsSchema.shape,
|
...DefaultOptionsSchema.shape,
|
||||||
type: z.literal("geometry"),
|
type: z.literal("geometry"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NodeInputPathSchema = z.object({
|
const NodeInputPathSchema = z.object({
|
||||||
...DefaultOptionsSchema.shape,
|
...DefaultOptionsSchema.shape,
|
||||||
type: z.literal("path"),
|
type: z.literal("path"),
|
||||||
});
|
});
|
@ -7,7 +7,7 @@ import {
|
|||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { relations } from "drizzle-orm/relations";
|
import { relations } from "drizzle-orm/relations";
|
||||||
import { usersTable } from "../user/user.schema.ts";
|
import { usersTable } from "../../user/user.schema.ts";
|
||||||
|
|
||||||
const bytea = customType<{
|
const bytea = customType<{
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
@ -25,6 +25,7 @@ export const nodeTable = pgTable("nodes", {
|
|||||||
nodeId: varchar().notNull(),
|
nodeId: varchar().notNull(),
|
||||||
content: bytea().notNull(),
|
content: bytea().notNull(),
|
||||||
definition: json().notNull(),
|
definition: json().notNull(),
|
||||||
|
hash: varchar({ length: 8 }).notNull(),
|
||||||
previous: integer(),
|
previous: integer(),
|
||||||
});
|
});
|
||||||
|
|
31
store/src/routes/node/schemas/types.ts
Normal file
31
store/src/routes/node/schemas/types.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { NodeInputSchema } from "./inputs.ts";
|
||||||
|
|
||||||
|
export type NodeId = `${string}/${string}/${string}`;
|
||||||
|
|
||||||
|
export const idRegex = /[a-z0-9-]+/i;
|
||||||
|
|
||||||
|
const idSchema = z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
new RegExp(
|
||||||
|
`^(${idRegex.source})/(${idRegex.source})/(${idRegex.source})$`,
|
||||||
|
),
|
||||||
|
"Invalid id format",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NodeDefinitionSchema = z
|
||||||
|
.object({
|
||||||
|
id: idSchema,
|
||||||
|
inputs: z.record(NodeInputSchema).optional(),
|
||||||
|
outputs: z.array(z.string()).optional(),
|
||||||
|
meta: z
|
||||||
|
.object({
|
||||||
|
description: z.string().optional(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.openapi("NodeDefinition");
|
||||||
|
|
||||||
|
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema>;
|
@ -1,69 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
import { NodeInputSchema } from "./inputs.ts";
|
|
||||||
|
|
||||||
export type NodeId = `${string}/${string}/${string}`;
|
|
||||||
|
|
||||||
export const NodeSchema = z.object({
|
|
||||||
id: z.number(),
|
|
||||||
type: z.string(),
|
|
||||||
props: z.record(z.union([z.number(), z.array(z.number())])).optional(),
|
|
||||||
meta: z
|
|
||||||
.object({
|
|
||||||
title: z.string().optional(),
|
|
||||||
lastModified: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
position: z.tuple([z.number(), z.number()]),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Node = z.infer<typeof NodeSchema>;
|
|
||||||
|
|
||||||
const partPattern = /[a-z-_]{3,32}/;
|
|
||||||
|
|
||||||
const idSchema = z
|
|
||||||
.string()
|
|
||||||
.regex(
|
|
||||||
new RegExp(
|
|
||||||
`^(${partPattern.source})/(${partPattern.source})/(${partPattern.source})$`,
|
|
||||||
),
|
|
||||||
"Invalid id format",
|
|
||||||
);
|
|
||||||
|
|
||||||
export const NodeDefinitionSchema = z
|
|
||||||
.object({
|
|
||||||
id: idSchema,
|
|
||||||
inputs: z.record(NodeInputSchema).optional(),
|
|
||||||
outputs: z.array(z.string()).optional(),
|
|
||||||
meta: z
|
|
||||||
.object({
|
|
||||||
description: z.string().optional(),
|
|
||||||
title: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
.openapi("NodeDefinition");
|
|
||||||
|
|
||||||
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema>;
|
|
||||||
|
|
||||||
export type Socket = {
|
|
||||||
node: Node;
|
|
||||||
index: number | string;
|
|
||||||
position: [number, number];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Edge = [Node, number, Node, string];
|
|
||||||
|
|
||||||
export const GraphSchema = z.object({
|
|
||||||
id: z.number().optional(),
|
|
||||||
meta: z
|
|
||||||
.object({
|
|
||||||
title: z.string().optional(),
|
|
||||||
lastModified: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
settings: z.record(z.any()).optional(),
|
|
||||||
nodes: z.array(NodeSchema),
|
|
||||||
edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Graph = z.infer<typeof GraphSchema> & { nodes: Node[] };
|
|
40
store/src/routes/node/worker/node.worker.ts
Normal file
40
store/src/routes/node/worker/node.worker.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
|
import { NodeDefinitionSchema } from "../schemas/types.ts";
|
||||||
|
import { WorkerMessage } from "./types.ts";
|
||||||
|
import { createWasmWrapper } from "./utils.ts";
|
||||||
|
|
||||||
|
const workerSelf = self as DedicatedWorkerGlobalScope & {
|
||||||
|
postMessage: (message: WorkerMessage) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function extractDefinition(wasmCode: ArrayBuffer) {
|
||||||
|
try {
|
||||||
|
const wasm = createWasmWrapper(wasmCode);
|
||||||
|
|
||||||
|
const definition = wasm.get_definition();
|
||||||
|
|
||||||
|
const p = NodeDefinitionSchema.safeParse(definition);
|
||||||
|
|
||||||
|
if (!p.success) {
|
||||||
|
workerSelf.postMessage({ action: "error", error: p.error });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
workerSelf.postMessage({ action: "result", result: p.data });
|
||||||
|
} catch (e) {
|
||||||
|
console.log("HEEERE", e);
|
||||||
|
workerSelf.postMessage({ action: "error", error: e });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = (e: MessageEvent<WorkerMessage>) => {
|
||||||
|
switch (e.data.action) {
|
||||||
|
case "extract-definition":
|
||||||
|
extractDefinition(e.data.content);
|
||||||
|
self.close();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown action: " + e.data.action);
|
||||||
|
}
|
||||||
|
};
|
21
store/src/routes/node/worker/types.ts
Normal file
21
store/src/routes/node/worker/types.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { NodeDefinition } from "../schemas/types.ts";
|
||||||
|
|
||||||
|
type ExtractDefinitionMessage = {
|
||||||
|
action: "extract-definition";
|
||||||
|
content: ArrayBuffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ErrorMessage = {
|
||||||
|
action: "error";
|
||||||
|
error: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResultMessage = {
|
||||||
|
action: "result";
|
||||||
|
result: NodeDefinition;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkerMessage =
|
||||||
|
| ErrorMessage
|
||||||
|
| ResultMessage
|
||||||
|
| ExtractDefinitionMessage;
|
@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck: Nocheck
|
// @ts-nocheck: Nocheck
|
||||||
import { NodeDefinition } from "./types.ts";
|
import { NodeDefinition } from "../schemas/types.ts";
|
||||||
|
|
||||||
const cachedTextDecoder = new TextDecoder("utf-8", {
|
const cachedTextDecoder = new TextDecoder("utf-8", {
|
||||||
ignoreBOM: true,
|
ignoreBOM: true,
|
@ -5,6 +5,6 @@ import { userRouter } from "./user/user.controller.ts";
|
|||||||
const router = new OpenAPIHono();
|
const router = new OpenAPIHono();
|
||||||
|
|
||||||
router.route("nodes", nodeRouter);
|
router.route("nodes", nodeRouter);
|
||||||
router.route("nodes", userRouter);
|
router.route("users", userRouter);
|
||||||
|
|
||||||
export { router };
|
export { router };
|
||||||
|
@ -22,7 +22,7 @@ export async function findUserByName(userName: string) {
|
|||||||
const users = await db
|
const users = await db
|
||||||
.select()
|
.select()
|
||||||
.from(usersTable)
|
.from(usersTable)
|
||||||
.where(eq(usersTable.name, userName));
|
.where(eq(usersTable.name, userName)).limit(1);
|
||||||
|
|
||||||
return users[0];
|
return users[0];
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user