feat: Update website, API, and infra (#164)

>Adds `maitred` in charge of handling automated game installs, updates,
and even execution.

>Not only that, we have the hosted stuff here
>- [x] AWS Task on ECS GPUs
>- [ ] Add a service to listen for game starts and stops
(docker-compose.yml)
>- [x] Add a queue for requesting a game to start
>- [x] Fix up the play/watch UI 

>TODO:
>- Add a README
>- Add an SST docs

Edit:

- This adds a new landing page, updates the homepage etc etc
>I forgot what the rest of the updated stuff are 😅
This commit is contained in:
Wanjohi
2025-02-11 12:26:35 +03:00
committed by GitHub
parent 93327bdf1a
commit 060718d8b0
139 changed files with 5814 additions and 5049 deletions

View File

@@ -1,6 +1,6 @@
import { createContext } from "./context";
import { VisibleError } from "./error";
export interface UserActor {
type: "user";
properties: {
@@ -21,8 +21,8 @@ export interface UserActor {
export interface DeviceActor {
type: "device";
properties: {
fingerprint: string;
id: string;
teamSlug: string;
hostname: string;
auth?:
| {
type: "personal";
@@ -47,7 +47,7 @@ export function useCurrentUser() {
const actor = ActorContext.use();
if (actor.type === "user") return {
id:actor.properties.userID,
token: actor.properties.accessToken
token: actor.properties.accessToken,
};
throw new VisibleError(
@@ -60,8 +60,8 @@ export function useCurrentUser() {
export function useCurrentDevice() {
const actor = ActorContext.use();
if (actor.type === "device") return {
fingerprint:actor.properties.fingerprint,
id: actor.properties.id
hostname:actor.properties.hostname,
teamSlug: actor.properties.teamSlug
};
throw new VisibleError(
"auth",

View File

@@ -0,0 +1,90 @@
import { z } from "zod"
import { Resource } from "sst";
import { doubleFn, fn } from "../utils";
import { AwsClient } from "aws4fetch";
import { DescribeTasksCommandOutput, StopTaskCommandOutput, type RunTaskCommandOutput } from "@aws-sdk/client-ecs";
export module Aws {
export const client = async () => {
return new AwsClient({
accessKeyId: Resource.AwsAccessKey.value,
secretAccessKey: Resource.AwsSecretKey.value,
region: "us-east-1",
});
}
export const EcsRunTask = fn(z.object({
cluster: z.string(),
count: z.number(),
taskDefinition: z.string(),
launchType: z.enum(["EC2", "FARGATE"]),
overrides: z.object({
containerOverrides: z.object({
name: z.string(),
environment: z.object({
name: z.string(),
value: z.string().or(z.number())
}).array()
}).array()
})
}), async (body) => {
const c = await client();
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
const res = await c.fetch(url, {
method: "POST",
headers: {
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.RunTask",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify(body)
})
return await res.json() as RunTaskCommandOutput
})
export const EcsDescribeTasks = fn(z.object({ tasks: z.string().array(), cluster: z.string() }), async (body) => {
const c = await client();
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
const res = await c.fetch(url, {
method: "POST",
headers: {
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.DescribeTasks",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify(body)
})
return await res.json() as DescribeTasksCommandOutput
})
export const EcsStopTask = fn(z.object({
cluster: z.string().optional(),
reason: z.string().optional(),
task: z.string()
}), async (body) => {
const c = await client();
const url = new URL(`https://ecs.${c.region}.amazonaws.com/`)
const res = await c.fetch(url, {
method: "POST",
headers: {
"X-Amz-Target": "AmazonEC2ContainerServiceV20141113.StopTask",
"Content-Type": "application/x-amz-json-1.1",
},
body: JSON.stringify(body)
})
return await res.json() as StopTaskCommandOutput
})
}

View File

@@ -5,15 +5,27 @@ export module Examples {
email: "john@example.com",
};
export const Task = {
id: "0bfcc712-df13-4454-81a8-fbee66eddca4",
taskID: "b8302fca2d224d91ab342a2e4ab926d3",
type: "AWS" as const, //or "on-premises",
lastStatus: "RUNNING" as const,
healthStatus: "UNKNOWN" as const,
startedAt: '2025-01-09T01:56:23.902Z',
lastUpdated: '2025-01-09T01:56:23.902Z',
stoppedAt: '2025-01-09T04:46:23.902Z'
}
export const Profile = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
username: "janedoe47",
status: "active" as const,
avatarUrl: "https://cdn.discordapp.com/avatars/xxxxxxx/xxxxxxx.png",
discriminator: 12, //it needs to be two digits
createdAt: '2025-01-04T11:56:23.902Z',
updatedAt: '2025-01-09T01:56:23.902Z'
}
export const Subscription = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
checkoutID: "0bfcb712-df43-4454-81a8-fbee66eddca4",
@@ -23,10 +35,10 @@ export module Examples {
// next: '2025-01-09T01:56:23.902Z',
canceledAt: '2025-02-09T01:56:23.902Z'
}
export const Team = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
owner: true,
// owner: true,
name: "Jane Doe's Games",
slug: "jane-does-games",
createdAt: '2025-01-04T11:56:23.902Z',
@@ -41,6 +53,13 @@ export module Examples {
deletedAt: '2025-01-09T01:56:23.902Z'
}
export const Instance = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
hostname: "a955e059f05d",
createdAt: '2025-01-04T11:56:23.902Z',
lastActive: '2025-01-09T01:56:23.902Z'
}
export const Game = {
id: '0bfcb712-df13-4454-81a8-fbee66eddca4',
name: "Control Ultimate Edition",
@@ -50,8 +69,7 @@ export module Examples {
export const Session = {
id: "0bfcb712-df13-4454-81a8-fbee66eddca4",
public: true,
name: 'Late night chilling with the squad',
startedAt: '2025-01-04T11:56:23.902Z',
endedAt: '2025-01-04T11:56:23.902Z'
endedAt: '2025-01-04T12:36:23.902Z'
}
}

View File

@@ -1,151 +1,151 @@
import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { id as createID } from "@instantdb/admin";
import { groupBy, map, pipe, values } from "remeda"
import { useCurrentDevice, useCurrentUser } from "../actor";
// import { z } from "zod"
// import { fn } from "../utils";
// import { Common } from "../common";
// import { Examples } from "../examples";
// import databaseClient from "../database"
// import { id as createID } from "@instantdb/admin";
// import { groupBy, map, pipe, values } from "remeda"
// import { useCurrentDevice, useCurrentUser } from "../actor";
export module Games {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Game.id,
}),
name: z.string().openapi({
description: "A human-readable name for the game, used for easy identification.",
example: Examples.Game.name,
}),
steamID: z.number().openapi({
description: "The Steam ID of the game, used to identify it during installation and runtime.",
example: Examples.Game.steamID,
})
})
.openapi({
ref: "Game",
description: "Represents a Steam game that can be installed and played on a machine.",
example: Examples.Game,
});
// export module Games {
// export const Info = z
// .object({
// id: z.string().openapi({
// description: Common.IdDescription,
// example: Examples.Game.id,
// }),
// name: z.string().openapi({
// description: "A human-readable name for the game, used for easy identification.",
// example: Examples.Game.name,
// }),
// steamID: z.number().openapi({
// description: "The Steam ID of the game, used to identify it during installation and runtime.",
// example: Examples.Game.steamID,
// })
// })
// .openapi({
// ref: "Game",
// description: "Represents a Steam game that can be installed and played on a machine.",
// example: Examples.Game,
// });
export type Info = z.infer<typeof Info>;
// export type Info = z.infer<typeof Info>;
export const create = fn(Info.pick({ name: true, steamID: true }), async (input) => {
const id = createID()
const db = databaseClient()
const device = useCurrentDevice()
// export const create = fn(Info.pick({ name: true, steamID: true }), async (input) => {
// const id = createID()
// const db = databaseClient()
// const device = useCurrentDevice()
await db.transact(
db.tx.games[id]!.update({
name: input.name,
steamID: input.steamID,
}).link({ machines: device.id })
)
// await db.transact(
// db.tx.games[id]!.update({
// name: input.name,
// steamID: input.steamID,
// }).link({ machines: device.id })
// )
// //
// return id
// })
return id
})
// export const list = async () => {
// const db = databaseClient()
// const user = useCurrentUser()
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
// const query = {
// $users: {
// $: { where: { id: user.id } },
// games: {}
// },
// }
const query = {
$users: {
$: { where: { id: user.id } },
games: {}
},
}
// const res = await db.query(query)
const res = await db.query(query)
// const games = res.$users[0]?.games
// if (games && games.length > 0) {
// const result = pipe(
// games,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// name: group[0].name,
// steamID: group[0].steamID,
// }))
// )
// return result
// }
// return null
// }
const games = res.$users[0]?.games
if (games && games.length > 0) {
const result = pipe(
games,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
steamID: group[0].steamID,
}))
)
return result
}
return null
}
// export const fromSteamID = fn(z.number(), async (steamID) => {
// const db = databaseClient()
export const fromSteamID = fn(z.number(), async (steamID) => {
const db = databaseClient()
// const query = {
// games: {
// $: {
// where: {
// steamID,
// }
// }
// }
// }
const query = {
games: {
$: {
where: {
steamID,
}
}
}
}
// const res = await db.query(query)
const res = await db.query(query)
// const games = res.games
const games = res.games
// if (games.length > 0) {
// const result = pipe(
// games,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// name: group[0].name,
// steamID: group[0].steamID,
// }))
// )
// return result[0]
// }
if (games.length > 0) {
const result = pipe(
games,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
name: group[0].name,
steamID: group[0].steamID,
}))
)
return result[0]
}
// return null
// })
return null
})
// export const linkToCurrentUser = fn(z.string(), async (steamID) => {
// const user = useCurrentUser()
// const db = databaseClient()
export const linkToCurrentUser = fn(z.string(), async (steamID) => {
const user = useCurrentUser()
const db = databaseClient()
// await db.transact(db.tx.games[steamID]!.link({ owners: user.id }))
await db.transact(db.tx.games[steamID]!.link({ owners: user.id }))
// return "ok"
// })
return "ok"
})
// export const unLinkFromCurrentUser = fn(z.number(), async (steamID) => {
// const user = useCurrentUser()
// const db = databaseClient()
export const unLinkFromCurrentUser = fn(z.number(), async (steamID) => {
const user = useCurrentUser()
const db = databaseClient()
// const query = {
// $users: {
// $: { where: { id: user.id } },
// games: {
// $: {
// where: {
// steamID,
// }
// }
// }
// },
// }
const query = {
$users: {
$: { where: { id: user.id } },
games: {
$: {
where: {
steamID,
}
}
}
},
}
// const res = await db.query(query)
// const games = res.$users[0]?.games
// if (games && games.length > 0) {
// const game = games[0] as Info
// await db.transact(db.tx.games[game.id]!.unlink({ owners: user.id }))
const res = await db.query(query)
const games = res.$users[0]?.games
if (games && games.length > 0) {
const game = games[0] as Info
await db.transact(db.tx.games[game.id]!.unlink({ owners: user.id }))
// return "ok"
// }
return "ok"
}
// return null
// })
return null
})
}
// }

View File

@@ -0,0 +1,83 @@
import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { id as createID } from "@instantdb/admin";
import { groupBy, map, pipe, values } from "remeda"
export module Instances {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Instance.id,
}),
hostname: z.string().openapi({
description: "The container's hostname",
example: Examples.Instance.hostname,
}),
createdAt: z.string().or(z.number()).openapi({
description: "The time this instances was registered on the network",
example: Examples.Instance.createdAt,
}),
lastActive: z.string().or(z.number()).optional().openapi({
description: "The time this instance was last seen on the network",
example: Examples.Instance.lastActive,
})
})
.openapi({
ref: "Instance",
description: "Represents a running container that is connected to the Nestri network..",
example: Examples.Instance,
});
export type Info = z.infer<typeof Info>;
export const create = fn(z.object({ hostname: z.string(), teamID: z.string() }), async (input) => {
const id = createID()
const now = new Date().toISOString()
const db = databaseClient()
await db.transact(
db.tx.instances[id]!.update({
hostname: input.hostname,
createdAt: now,
}).link({ owners: input.teamID })
)
return "ok"
})
export const fromTeamID = fn(z.string(), async (teamID) => {
const db = databaseClient()
const query = {
instances: {
$: {
where: {
owners: teamID
}
}
}
}
const res = await db.query(query)
const data = res.instances
if (data && data.length > 0) {
const result = pipe(
data,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
lastActive: group[0].lastActive,
hostname: group[0].hostname,
createdAt: group[0].createdAt
}))
)
return result
}
return null
})
}

View File

@@ -1,232 +1,232 @@
import { z } from "zod"
import { fn } from "../utils";
import { Common } from "../common";
import { Examples } from "../examples";
import { useCurrentUser } from "../actor";
import databaseClient from "../database"
import { id as createID } from "@instantdb/admin";
import { groupBy, map, pipe, values } from "remeda"
import { Games } from "../game"
export module Machines {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Machine.id,
}),
hostname: z.string().openapi({
description: "The Linux hostname that identifies this machine",
example: Examples.Machine.hostname,
}),
fingerprint: z.string().openapi({
description: "A unique identifier derived from the machine's Linux machine ID.",
example: Examples.Machine.fingerprint,
}),
createdAt: z.string().or(z.number()).openapi({
description: "Represents a machine running on the Nestri network, containing its identifying information and metadata.",
example: Examples.Machine.createdAt,
})
})
.openapi({
ref: "Machine",
description: "Represents a physical or virtual machine connected to the Nestri network..",
example: Examples.Machine,
});
// import { z } from "zod"
// import { fn } from "../utils";
// import { Games } from "../game"
// import { Common } from "../common";
// import { Examples } from "../examples";
// import { useCurrentUser } from "../actor";
// import databaseClient from "../database"
// import { id as createID } from "@instantdb/admin";
// import { groupBy, map, pipe, values } from "remeda"
// export module Machines {
// export const Info = z
// .object({
// id: z.string().openapi({
// description: Common.IdDescription,
// example: Examples.Machine.id,
// }),
// hostname: z.string().openapi({
// description: "The Linux hostname that identifies this machine",
// example: Examples.Machine.hostname,
// }),
// fingerprint: z.string().openapi({
// description: "A unique identifier derived from the machine's Linux machine ID.",
// example: Examples.Machine.fingerprint,
// }),
// createdAt: z.string().or(z.number()).openapi({
// description: "Represents a machine running on the Nestri network, containing its identifying information and metadata.",
// example: Examples.Machine.createdAt,
// })
// })
// .openapi({
// ref: "Machine",
// description: "Represents a physical or virtual machine connected to the Nestri network..",
// example: Examples.Machine,
// });
export type Info = z.infer<typeof Info>;
// export type Info = z.infer<typeof Info>;
export const create = fn(Info.pick({ fingerprint: true, hostname: true }), async (input) => {
const id = createID()
const now = new Date().toISOString()
const db = databaseClient()
await db.transact(
db.tx.machines[id]!.update({
fingerprint: input.fingerprint,
hostname: input.hostname,
createdAt: now,
//Just in case it had been previously deleted
deletedAt: undefined
})
)
// export const create = fn(Info.pick({ fingerprint: true, hostname: true }), async (input) => {
// const id = createID()
// const now = new Date().toISOString()
// const db = databaseClient()
// await db.transact(
// db.tx.machines[id]!.update({
// fingerprint: input.fingerprint,
// hostname: input.hostname,
// createdAt: now,
// //Just in case it had been previously deleted
// deletedAt: undefined
// })
// )
return id
})
// return id
// })
export const fromID = fn(z.string(), async (id) => {
const db = databaseClient()
// // export const fromID = fn(z.string(), async (id) => {
// const db = databaseClient()
const query = {
machines: {
$: {
where: {
id: id,
deletedAt: { $isNull: true }
}
}
}
}
// const query = {
// machines: {
// $: {
// where: {
// id: id,
// deletedAt: { $isNull: true }
// }
// }
// }
// }
const res = await db.query(query)
const machines = res.machines
// const res = await db.query(query)
// const machines = res.machines
if (machines && machines.length > 0) {
const result = pipe(
machines,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
fingerprint: group[0].fingerprint,
hostname: group[0].hostname,
createdAt: group[0].createdAt
}))
)
return result
}
// if (machines && machines.length > 0) {
// const result = pipe(
// machines,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// fingerprint: group[0].fingerprint,
// hostname: group[0].hostname,
// createdAt: group[0].createdAt
// }))
// )
// return result
// }
return null
})
// return null
// })
export const installedGames = fn(z.string(), async (id) => {
const db = databaseClient()
// export const installedGames = fn(z.string(), async (id) => {
// const db = databaseClient()
const query = {
machines: {
$: {
where: {
id: id,
deletedAt: { $isNull: true }
}
},
games: {}
}
}
// const query = {
// machines: {
// $: {
// where: {
// id: id,
// deletedAt: { $isNull: true }
// }
// },
// games: {}
// }
// }
const res = await db.query(query)
const machines = res.machines
// const res = await db.query(query)
// const machines = res.machines
if (machines && machines.length > 0) {
const games = machines[0]?.games as any
if (games.length > 0) {
return games as Games.Info[]
}
return null
}
// if (machines && machines.length > 0) {
// const games = machines[0]?.games as any
// if (games.length > 0) {
// return games as Games.Info[]
// }
// return null
// }
return null
})
// return null
// })
export const fromFingerprint = fn(z.string(), async (input) => {
const db = databaseClient()
// export const fromFingerprint = fn(z.string(), async (input) => {
// const db = databaseClient()
const query = {
machines: {
$: {
where: {
fingerprint: input,
deletedAt: { $isNull: true }
}
}
}
}
// const query = {
// machines: {
// $: {
// where: {
// fingerprint: input,
// deletedAt: { $isNull: true }
// }
// }
// }
// }
const res = await db.query(query)
// const res = await db.query(query)
const machines = res.machines
// const machines = res.machines
if (machines.length > 0) {
const result = pipe(
machines,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
fingerprint: group[0].fingerprint,
hostname: group[0].hostname,
createdAt: group[0].createdAt
}))
)
return result[0]
}
// if (machines.length > 0) {
// const result = pipe(
// machines,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// fingerprint: group[0].fingerprint,
// hostname: group[0].hostname,
// createdAt: group[0].createdAt
// }))
// )
// return result[0]
// }
return null
})
// return null
// })
export const list = async () => {
const user = useCurrentUser()
const db = databaseClient()
// export const list = async () => {
// const user = useCurrentUser()
// const db = databaseClient()
const query = {
$users: {
$: { where: { id: user.id } },
machines: {
$: {
where: {
deletedAt: { $isNull: true }
}
}
}
},
}
// const query = {
// $users: {
// $: { where: { id: user.id } },
// machines: {
// $: {
// where: {
// deletedAt: { $isNull: true }
// }
// }
// }
// },
// }
const res = await db.query(query)
// const res = await db.query(query)
const machines = res.$users[0]?.machines
if (machines && machines.length > 0) {
const result = pipe(
machines,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
fingerprint: group[0].fingerprint,
hostname: group[0].hostname,
createdAt: group[0].createdAt
}))
)
return result
}
return null
}
// const machines = res.$users[0]?.machines
// if (machines && machines.length > 0) {
// const result = pipe(
// machines,
// groupBy(x => x.id),
// values(),
// map((group): Info => ({
// id: group[0].id,
// fingerprint: group[0].fingerprint,
// hostname: group[0].hostname,
// createdAt: group[0].createdAt
// }))
// )
// return result
// }
// return null
// }
export const linkToCurrentUser = fn(z.string(), async (id) => {
const user = useCurrentUser()
const db = databaseClient()
// export const linkToCurrentUser = fn(z.string(), async (id) => {
// const user = useCurrentUser()
// const db = databaseClient()
await db.transact(db.tx.machines[id]!.link({ owner: user.id }))
// await db.transact(db.tx.machines[id]!.link({ owner: user.id }))
return "ok"
})
// return "ok"
// })
export const unLinkFromCurrentUser = fn(z.string(), async (id) => {
const user = useCurrentUser()
const db = databaseClient()
const now = new Date().toISOString()
// export const unLinkFromCurrentUser = fn(z.string(), async (id) => {
// const user = useCurrentUser()
// const db = databaseClient()
// const now = new Date().toISOString()
const query = {
$users: {
$: { where: { id: user.id } },
machines: {
$: {
where: {
id,
deletedAt: { $isNull: true }
}
}
}
},
}
// const query = {
// $users: {
// $: { where: { id: user.id } },
// machines: {
// $: {
// where: {
// id,
// deletedAt: { $isNull: true }
// }
// }
// }
// },
// }
const res = await db.query(query)
const machines = res.$users[0]?.machines
if (machines && machines.length > 0) {
const machine = machines[0] as Info
await db.transact(db.tx.machines[machine.id]!.update({ deletedAt: now }))
// const res = await db.query(query)
// const machines = res.$users[0]?.machines
// if (machines && machines.length > 0) {
// const machine = machines[0] as Info
// await db.transact(db.tx.machines[machine.id]!.update({ deletedAt: now }))
return "ok"
}
// return "ok"
// }
return null
})
// return null
// })
}
// }

View File

@@ -4,9 +4,15 @@ import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database";
import { groupBy, map, pipe, values } from "remeda"
import { id as createID } from "@instantdb/admin";
import { id as createID, } from "@instantdb/admin";
import { useCurrentUser } from "../actor";
export const userStatus = z.enum([
"active", //online and playing a game
"idle", //online and not playing
"offline",
]);
export module Profiles {
const MAX_ATTEMPTS = 50;
@@ -24,6 +30,10 @@ export module Profiles {
description: "The url to the profile picture.",
example: Examples.Profile.username,
}),
status: userStatus.openapi({
description: "Whether the user is active, idle or offline",
example: Examples.Profile.status
}),
discriminator: z.string().or(z.number()).openapi({
description: "The number discriminator for each username",
example: Examples.Profile.discriminator,
@@ -44,6 +54,7 @@ export module Profiles {
});
export type Info = z.infer<typeof Info>;
export type userStatus = z.infer<typeof userStatus>;
export const sanitizeUsername = (username: string): string => {
// Remove spaces and numbers
@@ -91,7 +102,8 @@ export module Profiles {
username: group[0].username,
createdAt: group[0].createdAt,
discriminator: group[0].discriminator,
updatedAt: group[0].updatedAt
updatedAt: group[0].updatedAt,
status: group[0].status as userStatus
}))
)
})
@@ -175,6 +187,7 @@ export module Profiles {
createdAt: now,
updatedAt: now,
discriminator,
status: "idle"
}).link({ owner: input.owner })
)
})
@@ -203,48 +216,197 @@ export module Profiles {
return `${profiles[0]?.username}#${profiles[0]?.discriminator}`;
}
export const getProfile = async (ownerID: string) => {
export const fromOwnerID = async (ownerID: string) => {
try {
const db = databaseClient()
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
owner: ownerID
}
},
const query = {
profiles: {
$: {
where: {
owner: ownerID
}
},
}
}
}
const res = await db.query(query)
const res = await db.query(query)
const profiles = res.profiles
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const profile = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator,
status: group[0].status as userStatus
}))
)
return profile[0]
} catch (error) {
console.log("user fromOwnerID", error)
return null
}
const profile = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator
}))
)
return profile[0]
}
export const fromID = async (id: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
id
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const profile = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator,
status: group[0].status as userStatus
}))
)
return profile[0]
} catch (error) {
console.log("user fromID", error)
return null
}
}
export const fromIDToOwner = async (id: string) => {
try {
const db = databaseClient()
const query = {
profiles: {
$: {
where: {
id
}
},
}
}
const res = await db.query(query)
const profiles = res.profiles as any
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
return profiles[0]!.owner as string
} catch (error) {
console.log("user fromID", error)
return null
}
}
export const getCurrentProfile = async () => {
const user = useCurrentUser()
const currentProfile = await getProfile(user.id);
const currentProfile = await fromOwnerID(user.id);
return currentProfile
}
};
export const setStatus = fn(userStatus, async (status) => {
try {
const user = useCurrentUser()
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(
db.tx.profiles[user.id]!.update({
status,
updatedAt: now
})
)
} catch (error) {
console.log("user setStatus error", error)
return null
}
})
export const list = async () => {
try {
const db = databaseClient()
// const ago = new Date(Date.now() - (60 * 1000 * 30)).toISOString()
const ago = new Date(Date.now() - (24 * 60 * 60 * 1000)).toISOString()
const query = {
profiles: {
$: {
limit: 10,
where: {
updatedAt: { $gt: ago },
},
order: {
updatedAt: "desc" as const,
},
}
}
}
const res = await db.query(query)
const profiles = res.profiles
if (!profiles || profiles.length === 0) {
throw new Error("No profiles were found");
}
const result = pipe(
profiles,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
username: group[0].username,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
avatarUrl: group[0].avatarUrl,
discriminator: group[0].discriminator,
status: group[0].status as userStatus
}))
)
return result
} catch (error) {
console.log("user list error", error)
return null
}
}
}

View File

@@ -1,6 +1,5 @@
import { z } from "zod"
import { fn } from "../utils";
import { Machines } from "../machine";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
@@ -15,10 +14,6 @@ export module Sessions {
description: Common.IdDescription,
example: Examples.Session.id,
}),
name: z.string().openapi({
description: "A human-readable name for the session to help identify it",
example: Examples.Session.name,
}),
public: z.boolean().openapi({
description: "If true, the session is publicly viewable by all users. If false, only authorized users can access it",
example: Examples.Session.public,
@@ -40,82 +35,31 @@ export module Sessions {
export type Info = z.infer<typeof Info>;
export const create = fn(z.object({ name: z.string(), public: z.boolean(), fingerprint: z.string(), steamID: z.number() }), async (input) => {
const id = createID()
const now = new Date().toISOString()
const db = databaseClient()
const user = useCurrentUser()
const machine = await Machines.fromFingerprint(input.fingerprint)
if (!machine) {
return { error: "Such a machine does not exist" }
export const create = fn(z.object({ public: z.boolean() }), async (input) => {
try {
const id = createID()
const db = databaseClient()
const user = useCurrentUser()
const now = new Date().toISOString()
await db.transact(
db.tx.sessions[id]!.update({
public: input.public,
startedAt: now,
}).link({ owner: user.id })
)
return id
} catch (err) {
return null
}
const games = await Machines.installedGames(machine.id)
if (!games) {
return { error: "The machine has no installed games" }
}
const result = pipe(
games,
groupBy(x => x.steamID === input.steamID ? "similar" : undefined),
)
if (!result.similar || result.similar.length == 0) {
return { error: "The machine does not have this game installed" }
}
await db.transact(
db.tx.sessions[id]!.update({
name: input.name,
public: input.public,
startedAt: now,
}).link({ owner: user.id, machine: machine.id, game: result.similar[0].id })
)
return { data: id }
})
export const list = async () => {
const user = useCurrentUser()
const db = databaseClient()
const query = {
$users: {
$: { where: { id: user.id } },
sessions: {}
},
}
const res = await db.query(query)
const sessions = res.$users[0]?.sessions
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
}
return null
}
export const getActive = async () => {
const user = useCurrentUser()
const db = databaseClient()
try {
const db = databaseClient()
const query = {
$users: {
$: { where: { id: user.id } },
const query = {
sessions: {
$: {
where: {
@@ -123,48 +67,15 @@ export module Sessions {
}
}
}
},
}
const res = await db.query(query)
const sessions = res.$users[0]?.sessions
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
}
return null
}
export const getPublicActive = async () => {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
endedAt: { $isNull: true },
public: true
}
}
}
}
const res = await db.query(query)
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No active sessions found")
}
const sessions = res.sessions
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
@@ -174,39 +85,37 @@ export module Sessions {
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
} catch (error) {
return null
}
return null
}
export const fromSteamID = fn(z.number(), async (steamID) => {
const db = databaseClient()
export const fromID = fn(z.string(), async (id) => {
try {
const db = databaseClient()
const query = {
games: {
$: {
where: {
steamID
}
},
const query = {
sessions: {
$: {
where: {
endedAt: { $isNull: true },
public: true
id: id,
}
}
}
}
}
const res = await db.query(query)
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
const sessions = res.games[0]?.sessions
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
@@ -216,32 +125,38 @@ export module Sessions {
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
} catch (err) {
console.log("sessions error", err)
return null
}
return null
})
export const fromID = fn(z.string(), async (id) => {
const db = databaseClient()
useCurrentUser()
export const fromTaskID = fn(z.string(), async (taskID) => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
id: id,
const query = {
sessions: {
$: {
where: {
task: taskID,
endedAt: { $isNull: true }
}
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
console.log("sessions", sessions)
if (sessions && sessions.length > 0) {
const result = pipe(
sessions,
groupBy(x => x.id),
@@ -251,42 +166,86 @@ export module Sessions {
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
name: group[0].name
}))
)
return result
return result[0]
} catch (err) {
console.log("sessions error", err)
return null
}
return null
})
export const end = fn(z.string(), async (id) => {
const user = useCurrentUser()
const db = databaseClient()
const now = new Date().toISOString()
try {
const db = databaseClient()
const now = new Date().toISOString()
const query = {
$users: {
$: { where: { id: user.id } },
const query = {
sessions: {
$: {
where: {
owner: user.id,
id,
}
}
}
},
}
},
}
const res = await db.query(query)
const sessions = res.$users[0]?.sessions
if (sessions && sessions.length > 0) {
const session = sessions[0] as Info
await db.transact(db.tx.sessions[session.id]!.update({ endedAt: now }))
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
await db.transact(db.tx.sessions[sessions[0]!.id]!.update({ endedAt: now }))
return "ok"
} catch (error) {
return null
}
return null
})
export const fromOwnerID = fn(z.string(), async (id) => {
try {
const db = databaseClient()
const query = {
sessions: {
$: {
where: {
owner: id,
endedAt: { $isNull: true }
}
}
}
}
const res = await db.query(query)
const sessions = res.sessions
if (!sessions || sessions.length === 0) {
throw new Error("No sessions were found");
}
const result = pipe(
sessions,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
endedAt: group[0].endedAt,
startedAt: group[0].startedAt,
public: group[0].public,
}))
)
return result[0]
} catch (err) {
console.log("session owner error", err)
return null
}
})
}

View File

@@ -59,15 +59,15 @@ export namespace Subscriptions {
export type Info = z.infer<typeof Info>;
export const list = async () => {
export const list = fn(z.string().optional(), async (userID) => {
const db = databaseClient()
const user = useCurrentUser()
const user = userID ? userID : useCurrentUser().id
const query = {
subscriptions: {
$: {
where: {
owner: user.id,
owner: user,
canceledAt: { $isNull: true }
}
},
@@ -96,7 +96,7 @@ export namespace Subscriptions {
)
return result
}
})
export const create = fn(Info.omit({ id: true, canceledAt: true }), async (input) => {
// const id = createID()
@@ -112,7 +112,7 @@ export namespace Subscriptions {
checkoutID: input.checkoutID,
}).link({ owner: user.id }))
const res = await db.auth.getUser({ id: user.id })
const profile = await Profiles.getProfile(user.id)
const profile = await Profiles.fromOwnerID(user.id)
if (profile) {
await Email.sendWelcome(res.email, profile.username)
}
@@ -123,7 +123,7 @@ export namespace Subscriptions {
const db = databaseClient()
await db.transact(db.tx.subscriptions[id]!.update({
canceledAt: new Date().toString()
canceledAt: new Date().toISOString()
}))
})

View File

@@ -0,0 +1,331 @@
import { z } from "zod";
import { fn } from "../utils";
import { Resource } from "sst";
import { Aws } from "../aws/client";
import { Common } from "../common";
import { Examples } from "../examples";
import databaseClient from "../database"
import { useCurrentUser } from "../actor";
import { id as createID } from "@instantdb/admin";
import { groupBy, map, pipe, values } from "remeda"
import { Sessions } from "../session";
export const lastStatus = z.enum([
"RUNNING",
"PENDING",
"UNKNOWN",
"STOPPED",
]);
export const taskType = z.enum([
"AWS",
"ON_PREMISES",
"UNKNOWN"
]);
export const healthStatus = z.enum([
"HEALTHY",
"UNHEALTHY",
"UNKNOWN",
]);
export type taskType = z.infer<typeof taskType>;
export type lastStatus = z.infer<typeof lastStatus>;
export type healthStatus = z.infer<typeof healthStatus>;
export module Tasks {
export const Info = z
.object({
id: z.string().openapi({
description: Common.IdDescription,
example: Examples.Task.id,
}),
type: taskType.openapi({
description: "Where this task is hosted on",
example: Examples.Task.type,
}),
taskID: z.string().openapi({
description: "The id of this task as seen on AWS",
example: Examples.Task.taskID,
}),
startedAt: z.string().or(z.number()).openapi({
description: "The time this task was started",
example: Examples.Task.startedAt,
}),
lastUpdated: z.string().or(z.number()).openapi({
description: "The time the information about this task was last updated",
example: Examples.Task.lastUpdated,
}),
stoppedAt: z.string().or(z.number()).optional().openapi({
description: "The time this task was stopped or quit",
example: Examples.Task.lastUpdated,
}),
lastStatus: lastStatus.openapi({
description: "The last registered status of this task",
example: Examples.Task.lastStatus,
}),
healthStatus: healthStatus.openapi({
description: "The health status of this task",
example: Examples.Task.healthStatus,
})
})
.openapi({
ref: "Subscription",
description: "Subscription to a Nestri product.",
example: Examples.Task,
});
export type Info = z.infer<typeof Info>;
export const list = async () => {
const db = databaseClient()
const user = useCurrentUser()
try {
const query = {
tasks: {
$: {
where: {
stoppedAt: { $isNull: true },
owner: user.id
}
},
}
}
const data = await db.query(query)
const response = data.tasks
if (!response || response.length === 0) {
throw new Error("No task for this user were found");
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
taskID: group[0].taskID,
type: group[0].type as taskType,
lastStatus: group[0].lastStatus as lastStatus,
healthStatus: group[0].healthStatus as healthStatus,
startedAt: group[0].startedAt,
stoppedAt: group[0].stoppedAt,
lastUpdated: group[0].lastUpdated,
}))
)
return result
} catch (e) {
return null
}
}
export const create = async () => {
const user = useCurrentUser()
try {
//TODO: Use a simpler way to set the session ID
// const sessionID = createID()
const sessionID = await Sessions.create({ public: true })
if (!sessionID) throw new Error("No session id was given");
const run = await Aws.EcsRunTask({
count: 1,
cluster: Resource.NestriGPUCluster.value,
taskDefinition: Resource.NestriGPUTask.value,
launchType: "EC2",
overrides: {
containerOverrides: [
{
name: "nestri",
environment: [
{
name: "NESTRI_ROOM",
value: sessionID
}
]
}
]
}
})
if (!run.tasks || run.tasks.length === 0) {
throw new Error(`No tasks were started`);
}
// Extract task details
const task = run.tasks[0];
const taskArn = task?.taskArn!;
const taskId = taskArn.split('/').pop()!; // Extract task ID from ARN
const taskStatus = task?.lastStatus;
const taskHealthStatus = task?.healthStatus;
const startedAt = task?.startedAt!;
const id = createID()
const db = databaseClient()
const now = new Date().toISOString()
await db.transact(db.tx.tasks[id]!.update({
taskID: taskId,
type: "AWS",
healthStatus: taskHealthStatus ? taskHealthStatus.toString() : "UNKNOWN",
startedAt: startedAt ? startedAt.toISOString() : now,
lastStatus: taskStatus,
lastUpdated: now,
}).link({ owner: user.id, sessions: sessionID }))
return id
} catch (e) {
console.error("error", e)
return null
}
}
export const fromID = fn(z.string(), async (taskID) => {
const db = databaseClient()
try {
const query = {
tasks: {
$: {
where: {
id: taskID,
stoppedAt: { $isNull: true }
}
},
}
}
const data = await db.query(query)
const response = data.tasks
if (!response || response.length === 0) {
throw new Error("No task with the given id was found");
}
const result = pipe(
response,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
taskID: group[0].taskID,
type: group[0].type as taskType,
lastStatus: group[0].lastStatus as lastStatus,
healthStatus: group[0].healthStatus as healthStatus,
startedAt: group[0].startedAt,
stoppedAt: group[0].stoppedAt,
lastUpdated: group[0].lastUpdated,
}))
)
return result[0]
} catch (error) {
return null
}
})
export const update = fn(z.string(), async (taskID) => {
try {
const db = databaseClient()
const query = {
tasks: {
$: {
where: {
id: taskID,
stoppedAt: { $isNull: true }
}
},
}
}
const data = await db.query(query)
const response = data.tasks
if (!response || response.length === 0) {
throw new Error("No task with the given taskID was found");
}
const now = new Date().toISOString()
const describeResponse = await Aws.EcsDescribeTasks({
tasks: [response[0]!.taskID],
cluster: Resource.NestriGPUCluster.value
})
if (!describeResponse.tasks || describeResponse.tasks.length === 0) {
throw new Error("No tasks were found");
}
const task = describeResponse.tasks[0]!
const updatedDb = {
healthStatus: task.healthStatus ? task.healthStatus : "UNKNOWN",
lastStatus: task.lastStatus ? task.lastStatus : "UNKNOWN",
lastUpdated: now,
}
await db.transact(db.tx.tasks[response[0]!.id]!.update({
...updatedDb
}))
const updatedRes = [{ ...response[0]!, ...updatedDb }]
const result = pipe(
updatedRes,
groupBy(x => x.id),
values(),
map((group): Info => ({
id: group[0].id,
taskID: group[0].taskID,
type: group[0].type as taskType,
lastStatus: group[0].lastStatus as lastStatus,
healthStatus: group[0].healthStatus as healthStatus,
startedAt: group[0].startedAt,
stoppedAt: group[0].stoppedAt,
lastUpdated: group[0].lastUpdated,
}))
)
return result
} catch (error) {
console.error("update error", error)
return null
}
})
export const stop = fn(z.object({ taskID: z.string(), id: z.string() }), async (input) => {
const db = databaseClient()
const now = new Date().toISOString()
try {
//TODO:Check whether they own this task first
const stopResponse = await Aws.EcsStopTask({
task: input.taskID,
cluster: Resource.NestriGPUCluster.value,
reason: "Client requested a shutdown"
})
if (!stopResponse.task) {
throw new Error(`No task was stopped`);
}
await db.transact(db.tx.tasks[input.id]!.update({
stoppedAt: now,
lastUpdated: now,
lastStatus: "STOPPED",
healthStatus: "UNKNOWN"
}))
return "ok"
} catch (error) {
console.error("stop error", error)
return null
}
})
}

View File

@@ -26,10 +26,10 @@ export namespace Teams {
description: "The time when this team was last edited",
example: Examples.Team.updatedAt,
}),
owner: z.boolean().openapi({
description: "Whether this team is owned by this user",
example: Examples.Team.owner,
}),
// owner: z.boolean().openapi({
// description: "Whether this team is owned by this user",
// example: Examples.Team.owner,
// }),
slug: z.string().openapi({
description: "This is the unique name identifier for the team",
example: Examples.Team.slug
@@ -112,11 +112,10 @@ export namespace Teams {
map((group): Info => ({
id: group[0].id,
name: group[0].name,
createdAt: group[0].createdAt,
slug: group[0].slug,
createdAt: group[0].createdAt,
updatedAt: group[0].updatedAt,
//@ts-expect-error
owner: group[0].owner === user.id
// owner: group[0].owner === user.id
}))
)

View File

@@ -10,4 +10,18 @@ export function fn<
};
result.schema = arg1;
return result;
}
export function doubleFn<
Arg1 extends ZodSchema,
Arg2 extends ZodSchema,
Callback extends (arg1: z.output<Arg1>, arg2: z.output<Arg2>) => any,
>(arg1: Arg1, arg2: Arg2, cb: Callback) {
const result = function (input: z.input<typeof arg1>, input2: z.input<typeof arg2>): ReturnType<Callback> {
const parsed = arg1.parse(input);
const parsed2 = arg2.parse(input2);
return cb.apply(cb, [parsed as any, parsed2 as any]);
};
result.schema = arg1;
return result;
}