From ca91d82f404c16b639ce752bf276ada2bbe94e8d Mon Sep 17 00:00:00 2001
From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com>
Date: Mon, 13 Jan 2025 19:46:15 +0100
Subject: [PATCH] feat(hub): improved ui for actors
---
examples/react/actor/deno.json | 2 +
examples/react/package.json | 3 +-
frontend/apps/hub/index.html | 79 ++++++-----
frontend/apps/hub/package.json | 3 +-
.../project/components/actors/actor-build.tsx | 53 +++++++
.../components/actors/actor-config-tab.tsx | 14 ++
.../components/actors/actor-network-tab.tsx | 81 -----------
.../components/actors/actor-network.tsx | 64 +++++++++
.../components/actors/actor-region.tsx | 31 ++--
.../components/actors/actor-rpc-tab.tsx | 24 ++++
.../project/components/actors/actor-rpc.tsx | 35 +++++
.../components/actors/actor-runtime-tab.tsx | 132 ------------------
.../components/actors/actor-runtime.tsx | 69 +++++++++
.../components/actors/actor-state-tab.tsx | 66 +++++++++
.../actors/actors-actor-details.tsx | 126 ++++++-----------
.../components/matchmaker/lobby-region.tsx | 54 ++++---
.../project/forms/actor-rpc-call-form.tsx | 112 +++++++++++++++
.../project/queries/actors/query-options.ts | 101 +++++++++++++-
frontend/apps/hub/src/layouts/root.tsx | 75 ++--------
frontend/apps/hub/vite.config.ts | 23 ++-
frontend/package.json | 8 +-
frontend/packages/components/package.json | 8 +-
.../components/src/code-mirror-container.tsx | 14 +-
frontend/packages/components/src/code.tsx | 53 ++-----
.../packages/components/src/ui/typography.tsx | 1 -
frontend/packages/icons/package.json | 7 +-
frontend/yarn.lock | 22 ++-
sdks/actor/common/deno.json | 3 +-
sdks/actor/common/package.json | 12 +-
sdks/actor/common/src/reflect.ts | 10 ++
sdks/actor/common/src/utils.ts | 28 ++++
sdks/actor/protocol/deno.jsonc | 3 +-
sdks/actor/protocol/package.json | 12 +-
sdks/actor/protocol/src/http/inspect.ts | 12 ++
sdks/actor/runtime/src/actor.ts | 39 +++++-
site/src/content/docs/state.mdx | 19 +++
36 files changed, 867 insertions(+), 531 deletions(-)
create mode 100644 frontend/apps/hub/src/domains/project/components/actors/actor-build.tsx
create mode 100644 frontend/apps/hub/src/domains/project/components/actors/actor-config-tab.tsx
delete mode 100644 frontend/apps/hub/src/domains/project/components/actors/actor-network-tab.tsx
create mode 100644 frontend/apps/hub/src/domains/project/components/actors/actor-network.tsx
create mode 100644 frontend/apps/hub/src/domains/project/components/actors/actor-rpc-tab.tsx
create mode 100644 frontend/apps/hub/src/domains/project/components/actors/actor-rpc.tsx
delete mode 100644 frontend/apps/hub/src/domains/project/components/actors/actor-runtime-tab.tsx
create mode 100644 frontend/apps/hub/src/domains/project/components/actors/actor-runtime.tsx
create mode 100644 frontend/apps/hub/src/domains/project/components/actors/actor-state-tab.tsx
create mode 100644 frontend/apps/hub/src/domains/project/forms/actor-rpc-call-form.tsx
create mode 100644 sdks/actor/common/src/reflect.ts
create mode 100644 sdks/actor/protocol/src/http/inspect.ts
diff --git a/examples/react/actor/deno.json b/examples/react/actor/deno.json
index e1f9c28f16..7dd9236f00 100644
--- a/examples/react/actor/deno.json
+++ b/examples/react/actor/deno.json
@@ -9,6 +9,7 @@
"@rivet-gg/actor-protocol/ws/to_client": "../../../sdks/actor/protocol/src/ws/to_client.ts",
"@rivet-gg/actor-protocol/ws/to_server": "../../../sdks/actor/protocol/src/ws/to_server.ts",
"@rivet-gg/actor-protocol/ws": "../../../sdks/actor/protocol/src/ws/mod.ts",
+ "@rivet-gg/actor-protocol/http/inspect": "../../../sdks/actor/protocol/src/http/inspect.ts",
"@rivet-gg/actor-core": "../../../sdks/actor/core/src/mod.ts",
"@rivet-gg/actor-core/": "../../../sdks/actor/core/src/",
"@rivet-gg/actor-client": "../../../sdks/actor/client/src/mod.ts",
@@ -16,6 +17,7 @@
"@rivet-gg/actor-common/": "../../../sdks/actor/common/src/",
"@rivet-gg/actor-common/log": "../../../sdks/actor/common/src/log.ts",
"@rivet-gg/actor-common/utils": "../../../sdks/actor/common/src/utils.ts",
+ "@rivet-gg/actor-common/reflect": "../../../sdks/actor/common/src/reflect.ts",
"@core/asyncutil": "jsr:@core/asyncutil@^1.2.0",
"@std/assert": "jsr:@std/assert@^1.0.8",
"@std/async": "jsr:@std/async@^1.0.9",
diff --git a/examples/react/package.json b/examples/react/package.json
index 5861f3d06d..c0bb66f040 100644
--- a/examples/react/package.json
+++ b/examples/react/package.json
@@ -24,5 +24,6 @@
"@types/react-dom": "^19",
"typescript": "^5",
"unenv": "npm:@jogit/tmp-unenv"
- }
+ },
+ "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
}
diff --git a/frontend/apps/hub/index.html b/frontend/apps/hub/index.html
index 5cca61fbfb..26d32a47b2 100644
--- a/frontend/apps/hub/index.html
+++ b/frontend/apps/hub/index.html
@@ -1,45 +1,50 @@
-
-
-
+
+
-
+
+
-
-
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
- Rivet
-
-
-
+
+
+
+
+
+
-
-
-
+ Rivet
+
+
+
+
+
+
+
diff --git a/frontend/apps/hub/package.json b/frontend/apps/hub/package.json
index 58e4fce751..831f19ec4f 100644
--- a/frontend/apps/hub/package.json
+++ b/frontend/apps/hub/package.json
@@ -16,6 +16,7 @@
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@hookform/resolvers": "^3.3.4",
+ "@rivet-gg/actor-protocol": "*",
"@rivet-gg/api": "file:vendor/rivet-gg-api.tgz",
"@rivet-gg/api-ee": "file:vendor/rivet-gg-api-ee.tgz",
"@rivet-gg/components": "workspace:*",
@@ -45,7 +46,7 @@
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
- "zod": "^3.22.4"
+ "zod": "^3.24"
},
"devDependencies": {
"@sentry/vite-plugin": "^2.22.2",
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-build.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-build.tsx
new file mode 100644
index 0000000000..960b6f4e80
--- /dev/null
+++ b/frontend/apps/hub/src/domains/project/components/actors/actor-build.tsx
@@ -0,0 +1,53 @@
+import { useSuspenseQuery } from "@tanstack/react-query";
+import { actorBuildQueryOptions } from "../../queries";
+import { useId } from "react";
+import { Dt, Dd, Dl, WithTooltip, CopyButton } from "@rivet-gg/components";
+import { ActorTags } from "./actor-tags";
+
+interface ActorBuildProps {
+ projectNameId: string;
+ environmentNameId: string;
+ buildId: string;
+}
+
+export function ActorBuild({ projectNameId, environmentNameId, buildId }: ActorBuildProps) {
+ const { data } = useSuspenseQuery(
+ actorBuildQueryOptions({
+ projectNameId,
+ environmentNameId,
+ buildId,
+ }),
+ );
+
+ const id = useId();
+
+ return (
+
+
+ Build
+
+
+
+ Id
+
+
+ {data.id.split("-")[0]}
+
+ }
+ />
+
+ Created At
+ {data.createdAt.toLocaleString()}
+ Tags
+ {Object.keys(data.tags).length > 0 ? : "None"}
+
+
+
+ );
+}
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-config-tab.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-config-tab.tsx
new file mode 100644
index 0000000000..aff0d37e17
--- /dev/null
+++ b/frontend/apps/hub/src/domains/project/components/actors/actor-config-tab.tsx
@@ -0,0 +1,14 @@
+import { ScrollArea } from "@rivet-gg/components";
+import { ActorNetwork, ActorNetworkProps } from "./actor-network";
+import { ActorRuntime, ActorRuntimeProps } from "./actor-runtime";
+
+interface ActorConfigTabProps extends ActorRuntimeProps, ActorNetworkProps {}
+
+export function ActorConfigTab(props: ActorConfigTabProps) {
+ return (
+
+
+
+
+ );
+}
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-network-tab.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-network-tab.tsx
deleted file mode 100644
index 335cc25ca7..0000000000
--- a/frontend/apps/hub/src/domains/project/components/actors/actor-network-tab.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import type { Rivet } from "@rivet-gg/api";
-import {
- Badge,
- Code,
- Dd,
- Dl,
- Dt,
- Flex,
- ScrollArea,
- SmallText,
-} from "@rivet-gg/components";
-import { Fragment } from "react/jsx-runtime";
-
-interface ActorNetworkTabProps extends Pick {}
-
-export function ActorNetworkTab({ network }: ActorNetworkTabProps) {
- return (
-
-
-
- Mode
-
- {network.mode}
-
-
-
- Ports
-
-
-
- {Object.keys(network.ports || {}).length === 0 ? (
-
No ports configured
- ) : (
- Object.entries(network.ports).map(
- ([name, config]) => (
-
- {name}
-
- {config.routing.guard ? (
-
- Guard
-
- ) : null}
- {config.routing.host ? (
-
- Host
-
- ) : null}
-
-
- Internal port
-
- {config.internalPort || "-"}
-
-
-
- Protocol
- {config.protocol}
-
-
- Hostname
- {config.hostname || "-"}
-
-
- Path
- {config.path || "-"}
-
-
- Port
- {config.port || "-"}
-
-
- ),
- )
- )}
-
-
-
-
- );
-}
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-network.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-network.tsx
new file mode 100644
index 0000000000..202b0eb316
--- /dev/null
+++ b/frontend/apps/hub/src/domains/project/components/actors/actor-network.tsx
@@ -0,0 +1,64 @@
+import type { Rivet } from "@rivet-gg/api";
+import { Badge, Dd, Dl, Dt, SmallText } from "@rivet-gg/components";
+import { Fragment } from "react";
+
+export interface ActorNetworkProps extends Pick {}
+
+const NETWORK_MODE_LABELS: Record = {
+ bridge: "Bridge",
+ host: "Host",
+};
+
+export function ActorNetwork({ network }: ActorNetworkProps) {
+ return (
+ <>
+
+
+ Network{" "}
+
+ {NETWORK_MODE_LABELS[network.mode]}
+
+
+
+ {Object.keys(network.ports || {}).length === 0 ? (
+
No ports configured
+ ) : (
+ Object.entries(network.ports).map(([name, config]) => (
+
+
+
+ {name}{" "}
+ {config.routing.guard ? (
+
+ Guard
+
+ ) : null}
+ {config.routing.host ? (
+
+ Host
+
+ ) : null}
+
+
+
+ Internal port
+ {config.internalPort || "-"}
+ Protocol
+ {config.protocol}
+ Hostname
+ {config.hostname || "-"}
+ Path
+ {config.path || "-"}
+ Port
+ {config.port || "-"}
+
+
+
+
+ ))
+ )}
+
+
+ >
+ );
+}
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-region.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-region.tsx
index d12a66ffef..ea5a559906 100644
--- a/frontend/apps/hub/src/domains/project/components/actors/actor-region.tsx
+++ b/frontend/apps/hub/src/domains/project/components/actors/actor-region.tsx
@@ -1,11 +1,7 @@
import { AssetImage, Flex, WithTooltip } from "@rivet-gg/components";
import { useSuspenseQuery } from "@tanstack/react-query";
import { actorRegionQueryOptions } from "../../queries";
-import {
- REGION_LABEL,
- getRegionEmoji,
- getRegionKey,
-} from "../matchmaker/lobby-region";
+import { REGION_LABEL, RegionIcon, getRegionKey } from "../matchmaker/lobby-region";
interface ActorRegionProps {
regionId: string;
@@ -14,12 +10,7 @@ interface ActorRegionProps {
showLabel?: boolean | "abbreviated";
}
-export function ActorRegion({
- projectNameId,
- regionId,
- environmentNameId,
- showLabel,
-}: ActorRegionProps) {
+export function ActorRegion({ projectNameId, regionId, environmentNameId, showLabel }: ActorRegionProps) {
const { data: region } = useSuspenseQuery(
actorRegionQueryOptions({ projectNameId, environmentNameId, regionId }),
);
@@ -29,13 +20,12 @@ export function ActorRegion({
if (showLabel) {
return (
-
- {showLabel === "abbreviated"
- ? regionKey.toUpperCase()
- : (REGION_LABEL[regionKey] ?? REGION_LABEL.unknown)}
+
+
+ {showLabel === "abbreviated"
+ ? regionKey.toUpperCase()
+ : (REGION_LABEL[regionKey] ?? REGION_LABEL.unknown)}
+
);
}
@@ -45,10 +35,7 @@ export function ActorRegion({
content={REGION_LABEL[regionKey] ?? REGION_LABEL.unknown}
trigger={
-
+
}
/>
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-rpc-tab.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-rpc-tab.tsx
new file mode 100644
index 0000000000..338568ba13
--- /dev/null
+++ b/frontend/apps/hub/src/domains/project/components/actors/actor-rpc-tab.tsx
@@ -0,0 +1,24 @@
+import { Accordion, ScrollArea } from "@rivet-gg/components";
+import { Rivet } from "@rivet-gg/api";
+import { useQuery } from "@tanstack/react-query";
+import { actorsRpcsQueryOptions } from "../../queries";
+import { ActorRpc } from "./actor-rpc";
+
+interface ActorRpcTabProps extends Rivet.actor.Actor {
+ projectNameId: string;
+ environmentNameId: string;
+}
+
+export function ActorRpcTab({ network, projectNameId, environmentNameId, id: actorId }: ActorRpcTabProps) {
+ const { data } = useQuery(actorsRpcsQueryOptions({ actorId, network, projectNameId, environmentNameId }));
+
+ return (
+
+ {data?.length === 0 ? (
+ No RPCs found.
+ ) : (
+ {data?.map((rpc) => )}
+ )}
+
+ );
+}
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-rpc.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-rpc.tsx
new file mode 100644
index 0000000000..2cdb51b5c8
--- /dev/null
+++ b/frontend/apps/hub/src/domains/project/components/actors/actor-rpc.tsx
@@ -0,0 +1,35 @@
+import { AccordionContent, AccordionItem, AccordionTrigger, JsonCode, Label } from "@rivet-gg/components";
+import * as ActorRpcCallForm from "@/domains/project/forms/actor-rpc-call-form";
+
+interface ActorRpcProps {
+ rpc: string;
+}
+
+export function ActorRpc({ rpc }: ActorRpcProps) {
+ return (
+
+ {rpc}
+
+ {
+ console.log("RPC called", values);
+ }}
+ defaultValues={{ arguments: [] }}
+ >
+
+
+
+
+
+
+
+ Response
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-runtime-tab.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-runtime-tab.tsx
deleted file mode 100644
index 8e28cb9008..0000000000
--- a/frontend/apps/hub/src/domains/project/components/actors/actor-runtime-tab.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import type { Rivet } from "@rivet-gg/api";
-import {
- Code,
- CopyArea,
- CopyButton,
- Dd,
- Dl,
- Dt,
- Flex,
- Grid,
- ScrollArea,
- SmallText,
- WithTooltip,
- formatDuration,
-} from "@rivet-gg/components";
-import { useSuspenseQuery } from "@tanstack/react-query";
-import { Fragment } from "react/jsx-runtime";
-import { actorBuildQueryOptions } from "../../queries";
-import { ActorTags } from "./actor-tags";
-
-interface ActorRuntimeTabProps
- extends Omit {
- createTs: Date | undefined;
- startTs: Date | undefined;
- destroyTs: Date | undefined;
- projectNameId: string;
- environmentNameId: string;
-}
-
-export function ActorRuntimeTab({
- projectNameId,
- environmentNameId,
- lifecycle,
- runtime,
- resources,
-}: ActorRuntimeTabProps) {
- const { data } = useSuspenseQuery(
- actorBuildQueryOptions({
- projectNameId,
- environmentNameId,
- buildId: runtime.build,
- }),
- );
-
- return (
-
-
-
- Kill timeout
- {formatDuration(lifecycle.killTimeout || 0)}
- Resources
-
- {resources.cpu / 1000} CPU cores, {resources.memory} MB
- RAM
-
- {data ? (
- <>
- Build
-
-
-
- Id
-
-
-
- {data.id.split("-")[0]}
-
-
- }
- />
-
- Created At
- {data.createdAt.toLocaleString()}
- Tags
-
- {Object.keys(data.tags).length > 0 ? (
-
- ) : (
- "None"
- )}
-
-
-
- >
- ) : (
- <>
- Build
- Unknown
- >
- )}
- Arguments
-
- {runtime.arguments?.length === 0 ? (
- No arguments provided.
- ) : (
- {runtime.arguments?.join(" ")}
- )}
-
- Environment
-
- {Object.keys(runtime.environment || {}).length === 0 ? (
- No environment variables set.
- ) : (
-
- {Object.entries(runtime.environment || {}).map(
- ([name, value]) => (
-
-
-
-
- ),
- )}
-
- )}
-
-
-
-
- );
-}
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-runtime.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-runtime.tsx
new file mode 100644
index 0000000000..f1a62d5cac
--- /dev/null
+++ b/frontend/apps/hub/src/domains/project/components/actors/actor-runtime.tsx
@@ -0,0 +1,69 @@
+import type { Rivet } from "@rivet-gg/api";
+import { Code, CopyArea, Dd, Dl, Dt, Flex, Grid, Skeleton, formatDuration } from "@rivet-gg/components";
+import { Fragment } from "react/jsx-runtime";
+import { ActorBuild } from "./actor-build";
+import { Suspense } from "react";
+
+export interface ActorRuntimeProps extends Omit {
+ createTs: Date | undefined;
+ startTs: Date | undefined;
+ destroyTs: Date | undefined;
+ projectNameId: string;
+ environmentNameId: string;
+}
+
+export function ActorRuntime({
+ projectNameId,
+ environmentNameId,
+ lifecycle,
+ runtime,
+ resources,
+}: ActorRuntimeProps) {
+ return (
+
+
+ Runtime
+
+
+
+ Kill timeout
+ {formatDuration(lifecycle.killTimeout || 0)}
+ Resources
+
+ {resources.cpu / 1000} CPU cores, {resources.memory} MB RAM
+
+ Arguments
+
+ {runtime.arguments?.length === 0 ? (
+ No arguments provided.
+ ) : (
+ {runtime.arguments?.join(" ")}
+ )}
+
+ Environment
+
+ {Object.keys(runtime.environment || {}).length === 0 ? (
+ No environment variables set.
+ ) : (
+
+ {Object.entries(runtime.environment || {}).map(([name, value]) => (
+
+
+
+
+ ))}
+
+ )}
+
+ }>
+
+
+
+
+
+ );
+}
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actor-state-tab.tsx b/frontend/apps/hub/src/domains/project/components/actors/actor-state-tab.tsx
new file mode 100644
index 0000000000..c74ca92d03
--- /dev/null
+++ b/frontend/apps/hub/src/domains/project/components/actors/actor-state-tab.tsx
@@ -0,0 +1,66 @@
+import { Rivet } from "@rivet-gg/api";
+import { Code, JsonCode, Link, ScrollArea, Skeleton } from "@rivet-gg/components";
+import { useQuery } from "@tanstack/react-query";
+
+import { actorStateQueryOptions } from "../../queries";
+
+interface ActorStateTabProps extends Rivet.actor.Actor {
+ projectNameId: string;
+ environmentNameId: string;
+}
+
+export function ActorStateTab({
+ id: actorId,
+ network,
+ projectNameId,
+ environmentNameId,
+}: ActorStateTabProps) {
+ const { data, isLoading, error } = useQuery(
+ actorStateQueryOptions({ actorId, network, projectNameId, environmentNameId }),
+ );
+
+ if (error) {
+ return (
+
+
+ Failed to fetch state data.
+
+ If this issue persists, please contact support.
+
+
+ );
+ }
+
+ return (
+
+ {isLoading ? (
+
+ ) : !data?.enabled ? (
+
+ State functionality is not enabled for this actor.
+ Enable it by adding _onInitialize
method.{" "}
+
+ Learn more
+
+ .
+
+ ) : (
+ <>
+
+
+ State is fetched every second. You can override the data shown above by implementing
+ _inspectState
method in your actor.{" "}
+
+ Learn more
+
+ .
+
+ >
+ )}
+
+ );
+}
diff --git a/frontend/apps/hub/src/domains/project/components/actors/actors-actor-details.tsx b/frontend/apps/hub/src/domains/project/components/actors/actors-actor-details.tsx
index 6b51213c8b..a03a292245 100644
--- a/frontend/apps/hub/src/domains/project/components/actors/actors-actor-details.tsx
+++ b/frontend/apps/hub/src/domains/project/components/actors/actors-actor-details.tsx
@@ -17,17 +17,15 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { formatISO } from "date-fns";
import { Suspense } from "react";
-import {
- actorErrorsQueryOptions,
- actorQueryOptions,
- useDestroyActorMutation,
-} from "../../queries";
+import { actorErrorsQueryOptions, actorQueryOptions, useDestroyActorMutation } from "../../queries";
import { ActorLogsTab } from "./actor-logs-tab";
-import { ActorNetworkTab } from "./actor-network-tab";
+import { ActorNetwork } from "./actor-network";
import { ActorRegion } from "./actor-region";
-import { ActorRuntimeTab } from "./actor-runtime-tab";
+import { ActorConfigTab } from "./actor-config-tab";
import { ActorStatus } from "./actor-status";
import { ActorTags } from "./actor-tags";
+import { ActorStateTab } from "./actor-state-tab";
+import { ActorRpcTab } from "./actor-rpc-tab";
interface ActorsActorDetailsProps {
projectNameId: string;
@@ -35,14 +33,8 @@ interface ActorsActorDetailsProps {
actorId: string;
}
-export function ActorsActorDetails({
- projectNameId,
- environmentNameId,
- actorId,
-}: ActorsActorDetailsProps) {
- const { data } = useSuspenseQuery(
- actorQueryOptions({ projectNameId, environmentNameId, actorId }),
- );
+export function ActorsActorDetails({ projectNameId, environmentNameId, actorId }: ActorsActorDetailsProps) {
+ const { data } = useSuspenseQuery(actorQueryOptions({ projectNameId, environmentNameId, actorId }));
const { data: hasError } = useQuery(
actorErrorsQueryOptions({ projectNameId, environmentNameId, actorId }),
@@ -70,9 +62,7 @@ export function ActorsActorDetails({
-
- An error occurred while fetching actor data.
-
+ An error occurred while fetching actor data.
}
>
@@ -99,12 +89,7 @@ export function ActorsActorDetails({
@@ -121,42 +106,27 @@ export function ActorsActorDetails({
-
- ID
+
+ ID
{data.id.split("-")[0]}
-
- Created
+
+ Created
{formatISO(data.createdAt)}
-
-
- Destroyed
+
+
+ Destroyed
- {data.destroyTs
- ? formatISO(data.destroyTs)
- : "-"}
+ {data.destroyTs ? formatISO(data.destroyTs) : "-"}
@@ -182,28 +152,19 @@ export function ActorsActorDetails({
Error
- {hasError ? (
-
- ) : null}
+ {hasError ? : null}
- Runtime
- Network
+ RPC
+ State
+ Config
-
+
+
- An error occurred while fetching
- actors's logs.
+ An error occurred while fetching actors's logs.
}
@@ -219,20 +180,12 @@ export function ActorsActorDetails({
-
+
+
- An error occurred while fetching actor's
- logs.
+ An error occurred while fetching actor's logs.
}
@@ -248,21 +201,26 @@ export function ActorsActorDetails({
-
-
+
-
-
+
+
+
+
+
diff --git a/frontend/apps/hub/src/domains/project/components/matchmaker/lobby-region.tsx b/frontend/apps/hub/src/domains/project/components/matchmaker/lobby-region.tsx
index f11206aecc..a03710484c 100644
--- a/frontend/apps/hub/src/domains/project/components/matchmaker/lobby-region.tsx
+++ b/frontend/apps/hub/src/domains/project/components/matchmaker/lobby-region.tsx
@@ -2,9 +2,10 @@ import { converEmojiToUriFriendlyString } from "@/lib/emoji";
import { AssetImage, Flex, WithTooltip } from "@rivet-gg/components";
import { useSuspenseQuery } from "@tanstack/react-query";
import { projectRegionQueryOptions } from "../../queries";
+import { faComputer, Icon, IconProp } from "@rivet-gg/icons";
-export const REGION_EMOJI: Record = {
- local: "π ",
+export const REGION_ICON: Record = {
+ local: faComputer,
unknown: "β",
atlanta: "πΊπΈ", // Atlanta
san_francisco: "πΊπΈ", // San Francisco
@@ -111,32 +112,32 @@ export function getRegionKey(regionNameId: string | undefined) {
return regionIdSplit[regionIdSplit.length - 1];
}
-export function getRegionEmoji(regionKey: string | undefined = "") {
- const regionEmoji = REGION_EMOJI[regionKey] ?? REGION_EMOJI.unknown;
- return `/icons/emoji/${converEmojiToUriFriendlyString(regionEmoji)}.svg`;
+export function RegionIcon({ region = "", ...props }: { region: string | undefined; className?: string }) {
+ const regionIcon = REGION_ICON[region] ?? REGION_ICON.unknown;
+
+ if (typeof regionIcon === "string") {
+ return (
+
+ );
+ }
+
+ return ;
}
-export function LobbyRegion({
- projectId,
- regionId,
- showLabel,
-}: LobbyRegionProps) {
- const { data: region } = useSuspenseQuery(
- projectRegionQueryOptions({ projectId, regionId }),
- );
+export function LobbyRegion({ projectId, regionId, showLabel }: LobbyRegionProps) {
+ const { data: region } = useSuspenseQuery(projectRegionQueryOptions({ projectId, regionId }));
const regionKey = getRegionKey(region?.regionNameId);
if (showLabel) {
return (
-
- {showLabel === "abbreviated"
- ? regionKey
- : (REGION_LABEL[regionKey] ?? REGION_LABEL.unknown)}
+
+
+ {showLabel === "abbreviated"
+ ? regionKey
+ : (REGION_LABEL[regionKey] ?? REGION_LABEL.unknown)}
+
);
}
@@ -146,22 +147,15 @@ export function LobbyRegion({
content={REGION_LABEL[regionKey] ?? REGION_LABEL.unknown}
trigger={
-
+
}
/>
);
}
-export function LobbyRegionIcon({
- regionNameId,
- className,
-}: { regionNameId: string; className?: string }) {
- const regionKey = getRegionKey(regionNameId);
- return ;
+export function LobbyRegionIcon({ regionNameId, className }: { regionNameId: string; className?: string }) {
+ return ;
}
export function LobbyRegionName({ regionNameId }: { regionNameId: string }) {
diff --git a/frontend/apps/hub/src/domains/project/forms/actor-rpc-call-form.tsx b/frontend/apps/hub/src/domains/project/forms/actor-rpc-call-form.tsx
new file mode 100644
index 0000000000..5f2784204c
--- /dev/null
+++ b/frontend/apps/hub/src/domains/project/forms/actor-rpc-call-form.tsx
@@ -0,0 +1,112 @@
+import {
+ Button,
+ FileInput,
+ Flex,
+ FormControl,
+ FormField,
+ FormFieldContext,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ Input,
+ createSchemaForm,
+ Text,
+ fileSize,
+ JsonCode,
+ JavaScriptCode,
+ Label,
+} from "@rivet-gg/components";
+import { Icon, faTrash } from "@rivet-gg/icons";
+import { type UseFormReturn, useFieldArray, useFormContext } from "react-hook-form";
+import z from "zod";
+
+const parsableJson = z.custom((value) => {
+ try {
+ JSON.parse(value);
+ return true;
+ } catch {
+ return false;
+ }
+}, "Invalid JSON");
+
+export const formSchema = z.object({
+ arguments: z.array(parsableJson),
+});
+
+export type FormValues = z.infer;
+export type SubmitHandler = (values: FormValues, form: UseFormReturn) => Promise;
+
+const { Form, Submit, Reset } = createSchemaForm(formSchema);
+export { Form, Submit };
+
+export const Arguments = () => {
+ const { control, register, setValue, formState } = useFormContext();
+ const { fields, append, remove } = useFieldArray({
+ name: "arguments",
+ control,
+ });
+
+ return (
+
+
+ Arguments
+
+ {fields.length === 0 ?
No arguments.
: null}
+ {fields.map((field, index) => (
+
+
+
+ Argument #{index + 1}
+
+
+ {
+ setValue(`arguments.${index}`, value);
+ }}
+ />
+ remove(index)}
+ >
+
+
+
+
+
+
+
+
+ ))}
+
append("")}>
+ Add argument
+
+
+ );
+};
+
+export const ExampleCall = ({ rpc }: { rpc: string }) => {
+ const { watch } = useFormContext();
+
+ const args = watch("arguments");
+ return (
+
+
+ Constructed Call
+
+
+
+
+ Reset
+
+ Call
+
+
+ );
+};
diff --git a/frontend/apps/hub/src/domains/project/queries/actors/query-options.ts b/frontend/apps/hub/src/domains/project/queries/actors/query-options.ts
index 355224edb0..6a5be1bba8 100644
--- a/frontend/apps/hub/src/domains/project/queries/actors/query-options.ts
+++ b/frontend/apps/hub/src/domains/project/queries/actors/query-options.ts
@@ -1,6 +1,7 @@
import { mergeWatchStreams } from "@/lib/watch-utilities";
import { rivetClient } from "@/queries/global";
import { getMetaWatchIndex } from "@/queries/utils";
+import { InspectResponseSchema } from "@rivet-gg/actor-protocol/http/inspect";
import { Rivet } from "@rivet-gg/api";
import {
type InfiniteData,
@@ -269,12 +270,12 @@ export const actorBuildTagsQueryOptions = ({
key,
value,
index: `${index}`,
- })),
+ }))
);
},
structuralSharing(oldData, newData) {
- const response =
- (newData as { key: string; value: string }[]) || [];
+ const response = (newData as { key: string; value: string }[]) ||
+ [];
const tags = new Map>();
@@ -388,3 +389,97 @@ export const actorRegionQueryOptions = ({
.find((region) => region.id === regionId),
});
};
+
+export const actorInspectQueryOptions = ({
+ projectNameId,
+ environmentNameId,
+ actorId,
+ network,
+}: {
+ projectNameId: string;
+ environmentNameId: string;
+ actorId: string;
+ network: Rivet.actor.Network;
+}) => {
+ const http = Object.values(network.ports).find((port) =>
+ port.protocol === "http"
+ );
+ return queryOptions({
+ queryKey: [
+ "project",
+ projectNameId,
+ "environment",
+ environmentNameId,
+ "actor",
+ actorId,
+ "inspect",
+ ],
+ queryFn: async ({ signal }) => {
+ const url = new URL(
+ `${http?.protocol}://${http?.hostname}:${http?.port}`,
+ );
+ url.pathname = http?.path + "/inspect";
+ const response = await fetch(url.href, { signal });
+ if (!response.ok) {
+ throw response;
+ }
+
+ const parsed = InspectResponseSchema.parse(await response.json());
+
+ // format the JSON for better readability
+ parsed.state.native = JSON.stringify(
+ JSON.parse(parsed.state.native),
+ null,
+ 2,
+ );
+
+ return parsed;
+ },
+ enabled: http !== undefined,
+ });
+};
+
+export const actorStateQueryOptions = ({
+ projectNameId,
+ environmentNameId,
+ actorId,
+ network,
+}: {
+ projectNameId: string;
+ environmentNameId: string;
+ actorId: string;
+ network: Rivet.actor.Network;
+}) => {
+ return queryOptions({
+ ...actorInspectQueryOptions({
+ projectNameId,
+ environmentNameId,
+ actorId,
+ network,
+ }),
+ refetchInterval: 1000,
+ select: (data) => data.state,
+ });
+};
+
+export const actorsRpcsQueryOptions = ({
+ projectNameId,
+ environmentNameId,
+ actorId,
+ network,
+}: {
+ projectNameId: string;
+ environmentNameId: string;
+ actorId: string;
+ network: Rivet.actor.Network;
+}) => {
+ return queryOptions({
+ ...actorInspectQueryOptions({
+ projectNameId,
+ environmentNameId,
+ actorId,
+ network,
+ }),
+ select: (data) => data.rpcs,
+ });
+};
diff --git a/frontend/apps/hub/src/layouts/root.tsx b/frontend/apps/hub/src/layouts/root.tsx
index 2bc508e07f..da8a0332db 100644
--- a/frontend/apps/hub/src/layouts/root.tsx
+++ b/frontend/apps/hub/src/layouts/root.tsx
@@ -3,13 +3,7 @@ import { NavItem } from "@/components/header/nav-item";
import { usePageLayout } from "@/lib/compute-page-layout";
import { publicUrl } from "@/lib/utils";
import { cn } from "@rivet-gg/components";
-import {
- Icon,
- faBluesky,
- faDiscord,
- faGithub,
- faXTwitter,
-} from "@rivet-gg/icons";
+import { Icon, faBluesky, faDiscord, faGithub, faXTwitter } from "@rivet-gg/icons";
import type { PropsWithChildren, ReactNode } from "react";
import { Header as UiHeader } from "../components/header/header";
@@ -22,11 +16,7 @@ const Root = ({ children }: RootProps) => {
};
const Main = ({ children }: RootProps) => {
- return (
-
- {children}
-
- );
+ return {children} ;
};
const VisibleInFull = ({ children }: PropsWithChildren) => {
@@ -34,8 +24,7 @@ const VisibleInFull = ({ children }: PropsWithChildren) => {
return (
@@ -46,9 +35,7 @@ const VisibleInFull = ({ children }: PropsWithChildren) => {
const Header = () => {
const layout = usePageLayout();
- return (
-
- );
+ return
;
};
const Footer = () => {
@@ -58,11 +45,7 @@ const Footer = () => {
-
+
© {new Date().getFullYear()}
{
-
+
-
+
-
+
-
+
-
+
Home
-
+
Help
-
+
Pricing
-
+
Docs
{
theme: "css-variables",
transformers: [transformerNotationFocus()],
});
- return `export default ${JSON.stringify(output)};export const source = ${JSON.stringify(code)}`;
+ return `export default ${
+ JSON.stringify(output)
+ };export const source = ${JSON.stringify(code)}`;
}
},
};
diff --git a/frontend/package.json b/frontend/package.json
index 22a2b07ff7..91682b9ef8 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -2,7 +2,10 @@
"private": true,
"packageManager": "yarn@4.2.2",
"name": "hub",
- "workspaces": ["packages/*", "apps/*"],
+ "workspaces": [
+ "packages/*",
+ "apps/*"
+ ],
"scripts": {
"start": "npx turbo dev",
"build": "npx turbo build",
@@ -12,5 +15,8 @@
"@biomejs/biome": "1.9.4",
"lefthook": "^1.6.12",
"turbo": "^2.0.1"
+ },
+ "resolutions": {
+ "@rivet-gg/actor-protocol": "portal:../sdks/actor/protocol"
}
}
diff --git a/frontend/packages/components/package.json b/frontend/packages/components/package.json
index e95622c2b2..da5e05a7ef 100644
--- a/frontend/packages/components/package.json
+++ b/frontend/packages/components/package.json
@@ -3,7 +3,11 @@
"private": true,
"version": "1.0.0",
"type": "module",
- "files": ["dist", "src", "public"],
+ "files": [
+ "dist",
+ "src",
+ "public"
+ ],
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"sideEffects": false,
@@ -72,7 +76,7 @@
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
- "zod": "^3.23.8"
+ "zod": "^3.24"
},
"devDependencies": {
"@types/mime": "^4.0.0",
diff --git a/frontend/packages/components/src/code-mirror-container.tsx b/frontend/packages/components/src/code-mirror-container.tsx
index 689af0b0f8..ab5fa21df8 100644
--- a/frontend/packages/components/src/code-mirror-container.tsx
+++ b/frontend/packages/components/src/code-mirror-container.tsx
@@ -1,14 +1,6 @@
import { type ComponentProps, forwardRef } from "react";
+import { cn } from "./lib/utils";
-export const CodeMirrorContainer = forwardRef<
- HTMLDivElement,
- ComponentProps<"div">
->((props, ref) => {
- return (
-
- );
+export const CodeMirrorContainer = forwardRef>((props, ref) => {
+ return
;
});
diff --git a/frontend/packages/components/src/code.tsx b/frontend/packages/components/src/code.tsx
index 61089593ff..2944029e02 100644
--- a/frontend/packages/components/src/code.tsx
+++ b/frontend/packages/components/src/code.tsx
@@ -5,9 +5,7 @@ import { EditorView } from "@codemirror/view";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@radix-ui/react-tabs";
import { Icon, faCopy, faFile } from "@rivet-gg/icons";
import { githubDark } from "@uiw/codemirror-theme-github";
-import ReactCodeMirror, {
- type ReactCodeMirrorProps,
-} from "@uiw/react-codemirror";
+import ReactCodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
import { Children, type ReactElement, cloneElement, forwardRef } from "react";
import { CodeMirrorContainer } from "./code-mirror-container";
import { CopyButton } from "./copy-area";
@@ -20,16 +18,12 @@ import { WithTooltip } from "./ui/tooltip";
interface JsonCodeProps extends ReactCodeMirrorProps {}
export const JsonCode = forwardRef(
- ({ value, extensions = [], ...props }, ref) => {
+ ({ value, extensions = [], className, ...props }, ref) => {
return (
-
+
@@ -45,11 +39,7 @@ export const JavaScriptCode = forwardRef(
@@ -94,26 +84,16 @@ const getChildIdx = (child: ReactElement) =>
export function CodeGroup({ children, className }: CodeGroupProps) {
return (
-
+
- div]:!table" }}
- >
+ div]:!table" }}>
{Children.map(children, (child) => {
const idx = getChildIdx(child);
return (
{child.props.title ||
- languageNames[
- child.props.language || "bash"
- ] ||
+ languageNames[child.props.language || "bash"] ||
"Code"}
);
@@ -144,21 +124,12 @@ interface CodeFrameProps {
code?: string;
children?: ReactElement;
}
-export const CodeFrame = ({
- children,
- file,
- language,
- code,
- title,
- isInGroup,
-}: CodeFrameProps) => {
+export const CodeFrame = ({ children, file, language, code, title, isInGroup }: CodeFrameProps) => {
return (
- {children
- ? cloneElement(children, { escaped: true })
- : null}
+ {children ? cloneElement(children, { escaped: true }) : null}
@@ -170,9 +141,7 @@ export const CodeFrame = ({
{file}
>
) : isInGroup ? null : (
-
- {title || languageNames[language]}
-
+
{title || languageNames[language]}
)}
maxSize) {
+ throw new Error(
+ `JSON object exceeds size limit of ${maxSize} bytes.`,
+ );
+ }
+
+ return value;
+ }
+
+ return JSON.stringify(obj, replacer);
+}
diff --git a/sdks/actor/protocol/deno.jsonc b/sdks/actor/protocol/deno.jsonc
index b2dfdea0f1..994228926b 100644
--- a/sdks/actor/protocol/deno.jsonc
+++ b/sdks/actor/protocol/deno.jsonc
@@ -4,7 +4,8 @@
"./ws/to_server": "./src/ws/to_server.ts",
"./ws/to_client": "./src/ws/to_client.ts",
"./ws": "./src/ws/mod.ts",
- "./http/rpc": "./src/http/rpc.ts"
+ "./http/rpc": "./src/http/rpc.ts",
+ "./http/inspect": "./src/http/inspect.ts"
},
"imports": {
"zod": "npm:zod@^3.24.1"
diff --git a/sdks/actor/protocol/package.json b/sdks/actor/protocol/package.json
index 79a21a64cc..407109c31e 100644
--- a/sdks/actor/protocol/package.json
+++ b/sdks/actor/protocol/package.json
@@ -15,6 +15,16 @@
"default": "./dist/http/rpc.cjs"
}
},
+ "./http/inspect": {
+ "import": {
+ "types": "./dist/http/inspect.d.ts",
+ "default": "./dist/http/inspect.js"
+ },
+ "require": {
+ "types": "./dist/http/inspect.d.cts",
+ "default": "./dist/http/inspect.cjs"
+ }
+ },
"./ws": {
"import": {
"types": "./dist/ws/mod.d.ts",
@@ -48,7 +58,7 @@
},
"sideEffects": false,
"scripts": {
- "build": "tsup src/http/rpc.ts src/ws/mod.ts src/ws/to_client.ts src/ws/to_server.ts",
+ "build": "tsup src/http/rpc.ts src/http/inspect.ts src/ws/mod.ts src/ws/to_client.ts src/ws/to_server.ts",
"check-types": "tsc --noEmit"
},
"dependencies": {
diff --git a/sdks/actor/protocol/src/http/inspect.ts b/sdks/actor/protocol/src/http/inspect.ts
new file mode 100644
index 0000000000..09d1bc148e
--- /dev/null
+++ b/sdks/actor/protocol/src/http/inspect.ts
@@ -0,0 +1,12 @@
+import { z } from "zod";
+
+export const InspectResponseSchema = z.object({
+ rpcs: z.array(z.string()),
+ state: z.object({
+ enabled: z.boolean(),
+ native: z.string(),
+ }),
+ connections: z.number(),
+});
+
+export type InspectResponse = z.infer;
diff --git a/sdks/actor/runtime/src/actor.ts b/sdks/actor/runtime/src/actor.ts
index bf3c8bcbe5..ae5c23add6 100644
--- a/sdks/actor/runtime/src/actor.ts
+++ b/sdks/actor/runtime/src/actor.ts
@@ -1,14 +1,17 @@
import { Lock } from "@core/asyncutil/lock";
import { setupLogging } from "@rivet-gg/actor-common/log";
-import { assertUnreachable } from "@rivet-gg/actor-common/utils";
+import { assertUnreachable, safeStringify } from "@rivet-gg/actor-common/utils";
+import { listObjectMethods } from "@rivet-gg/actor-common/reflect";
import type { ActorContext, Metadata } from "@rivet-gg/actor-core";
import { ProtocolFormatSchema } from "@rivet-gg/actor-protocol/ws";
import type * as wsToClient from "@rivet-gg/actor-protocol/ws/to_client";
import * as wsToServer from "@rivet-gg/actor-protocol/ws/to_server";
+import { type InspectResponse } from "@rivet-gg/actor-protocol/http/inspect";
import { assertExists } from "@std/assert/exists";
import { deadline } from "@std/async/deadline";
import type { Logger } from "@std/log/get-logger";
import { Hono, type Context as HonoContext } from "hono";
+import { cors } from "hono/cors";
import { upgradeWebSocket } from "hono/deno";
import type { WSEvents } from "hono/ws";
import onChange from "on-change";
@@ -385,6 +388,23 @@ export abstract class Actor<
return c.text("This is a Rivet Actor\n\nLearn more at https://rivet.gg");
});
+
+ app.use("/inspect", cors({origin: (origin) => {
+ // Allow localhost:5080 for development, production domain (hub.rivet.gg), and dynamic preview domains (*.rivet-hub-7jb.pages.dev)
+ // TODO: make this configurable, (ask manager for configured origins?)
+ return origin.includes("localhost:5080") || origin.includes("hub.rivet.gg") || origin.includes(".rivet-hub-7jb.pages.dev") ? origin : undefined;
+ } })).get((c) => {
+ const response = {
+ rpcs: this.#rpcNames,
+ state: {
+ enabled: this.#stateEnabled,
+ native: this._inspectState()
+ },
+ connections: this.#connections.size
+ } satisfies InspectResponse;
+ return c.json(response);
+ })
+
//app.post("/rpc/:name", this.#pandleRpc.bind(this));
app.get("/connect", upgradeWebSocket(this.#handleWebSocket.bind(this)));
@@ -761,6 +781,12 @@ export abstract class Actor<
}
}
+ get #rpcNames(): string[] {
+ return listObjectMethods(this).filter((name): name is string =>
+ typeof name === "string" && this.#isValidRpc(name),
+ );
+ }
+
// MARK: Lifecycle hooks
/**
* Hook called when the actor is first created. This method should return the initial state of the actor. The state can be access with `this._state`.
@@ -839,6 +865,17 @@ export abstract class Actor<
*/
protected _onDisconnect?(connection: Connection): void | Promise;
+ /**
+ * Safely transforms the actor state into a string for debugging purposes.
+ */
+ protected _inspectState(): string {
+ try {
+ return safeStringify(this.#stateRaw, 128_000_000 /* 128 MB */);
+ } catch (error) {
+ return `Error inspecting state: ${error}`;
+ }
+ }
+
// MARK: Exposed methods
/**
* Gets metadata associated with this actor.
diff --git a/site/src/content/docs/state.mdx b/site/src/content/docs/state.mdx
index f66b9e1bab..a579edb8f0 100644
--- a/site/src/content/docs/state.mdx
+++ b/site/src/content/docs/state.mdx
@@ -84,6 +84,25 @@ State is constrained to the available memory (see [limitations](/docs/limitation
Only JSON-serializable types can be stored in state. State is persisted under the hood in a compact, binary format. This is because JavaScript classes cannot be serialized & deserialized.
+### Debugging
+
+To debug state, you can use visit the Rivet dashboard and view the state of each actor. This can be useful for understanding the current state of an actor and diagnosing issues. You can also override the method that's used to preview the state in the dashboard. Make sure to not exceed the maximum size of 125MB for the state preview.
+
+```typescript {{ "title": "actor.ts" }}
+
+export default class Counter extends Actor {
+ // Override the method used to inspect the state
+ override _inspectState() {
+ return {
+ count: this._state.count,
+ custom: "value",
+ nested: { key: "value" }
+ timestamp: Date.now(),
+ };
+ }
+}
+```
+
## Local Key-Value State
KV state is used for storing complex or large datasets that cannot fit into memory. You can access local KV using `this._kv`. The KV data is isolated to this actor and cannot be accessed from outside of it.