mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-04 14:42:42 +02:00
feat(session): a run picks its own size
POST /session takes an optional tier. The host starts the run at that size, the allowance is checked against it, and the run is billed at it. A run asked for without one runs at its box's size, as before. The size belongs to the run rather than the box: a box is a game's install on a machine, and the size is chosen each time it is played, so playing bigger never means a second box and a second install.
This commit is contained in:
@@ -162,6 +162,13 @@
|
||||
"when": 1791537816909,
|
||||
"tag": "0022_session_stop_requested",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 23,
|
||||
"version": "7",
|
||||
"when": 1791629955619,
|
||||
"tag": "0023_session_tier",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -59,6 +59,10 @@ export namespace Session {
|
||||
'Current connect ticket. Null before one has been minted, and null again once the run has stopped — a run that is not there has no address',
|
||||
example: Examples.Session.ticket
|
||||
}),
|
||||
tier: z.enum(BoxTier.enumValues).nullable().optional().meta({
|
||||
description: 'The size this run was asked for. Null runs at the size of its box',
|
||||
example: 'md'
|
||||
}),
|
||||
timeStarted: z.string().nullable().optional().meta({
|
||||
description: 'When the box actually started, not when the row appeared',
|
||||
example: Examples.Session.timeStarted
|
||||
@@ -91,7 +95,7 @@ export namespace Session {
|
||||
export type Info = z.infer<typeof Info>;
|
||||
|
||||
export const create = fn(
|
||||
Info.pick({ id: true, boxId: true, gameId: true, linkedAccountId: true }).extend({
|
||||
Info.pick({ id: true, boxId: true, gameId: true, linkedAccountId: true, tier: true }).extend({
|
||||
trial: z.boolean().optional()
|
||||
}),
|
||||
async (input) => {
|
||||
@@ -103,6 +107,7 @@ export namespace Session {
|
||||
boxId: input.boxId,
|
||||
gameId: input.gameId,
|
||||
linkedAccountId: input.linkedAccountId,
|
||||
tier: input.tier ?? null,
|
||||
trial: input.trial ?? false
|
||||
})
|
||||
.returning()
|
||||
@@ -133,7 +138,7 @@ export namespace Session {
|
||||
* answered is a timing detail.
|
||||
*/
|
||||
export const request = fn(
|
||||
Info.pick({ id: true, boxId: true, gameId: true, linkedAccountId: true }).extend({
|
||||
Info.pick({ id: true, boxId: true, gameId: true, linkedAccountId: true, tier: true }).extend({
|
||||
trial: z.boolean().optional()
|
||||
}),
|
||||
async (input) => {
|
||||
@@ -442,7 +447,8 @@ export namespace Session {
|
||||
kind: 'session.start',
|
||||
sessionId: row.session.id,
|
||||
boxId: row.box.id,
|
||||
boxTier: row.box.tier as z.infer<typeof StartJob>['boxTier'],
|
||||
// The run's own size when it was given one.
|
||||
boxTier: (row.session.tier ?? row.box.tier) as z.infer<typeof StartJob>['boxTier'],
|
||||
gameId: row.game.id,
|
||||
steamAppId: row.game.steamAppId,
|
||||
linkedAccountId: row.session.linkedAccountId,
|
||||
@@ -825,7 +831,7 @@ export namespace Session {
|
||||
await Burn.start({
|
||||
teamId: payer.teamId,
|
||||
sessionId: moved.id,
|
||||
tier: payer.tier,
|
||||
tier: moved.tier ?? payer.tier,
|
||||
hostClass: payer.hostClass
|
||||
});
|
||||
} else if (
|
||||
@@ -937,6 +943,7 @@ export namespace Session {
|
||||
gameId: input.gameId,
|
||||
linkedAccountId: input.linkedAccountId,
|
||||
state: input.state as Info['state'],
|
||||
tier: input.tier as Info['tier'],
|
||||
ticket: input.ticket,
|
||||
timeStarted: input.timeStarted?.toISOString() ?? null,
|
||||
timeStopped: input.timeStopped?.toISOString() ?? null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { boolean, index, pgEnum, pgTable, text, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
|
||||
import { BoxTable } from '../box/box.sql.js';
|
||||
import { BoxTable, BoxTier } from '../box/box.sql.js';
|
||||
import { id, timestamps, ulid, utc } from '../db/types.js';
|
||||
import { GameTable } from '../game/game.sql.js';
|
||||
import { LinkedAccountTable } from '../user/linked-account.sql.js';
|
||||
@@ -53,6 +53,11 @@ export const SessionTable = pgTable(
|
||||
.notNull()
|
||||
.references(() => LinkedAccountTable.id, { onDelete: 'restrict' }),
|
||||
state: SessionState('state').notNull().default('requested'),
|
||||
// The size of this run, picked when it is asked for. Null for a run
|
||||
// asked for without one, which runs at its box's size. On the run and
|
||||
// not the box, because the box is a game's install on a machine and
|
||||
// a size is chosen each time it is played.
|
||||
tier: BoxTier('tier'),
|
||||
/**
|
||||
* The current iroh connect ticket, or null before `neshub` has minted
|
||||
* one. Rewritten as addresses are discovered; never append-only.
|
||||
|
||||
@@ -250,6 +250,22 @@ describe('Session jobs', () => {
|
||||
expect(jobs[0]!.linkedAccountId).toBe(owner.linkedAccountId);
|
||||
});
|
||||
|
||||
test('a run asked for at a size starts at that size, not its box’s', async () => {
|
||||
const { owner, machineId, box, gameId } = await scene('ses-job-tier', 5411);
|
||||
const session = await Session.create({
|
||||
id: Identifier.ascending('session'),
|
||||
boxId: box.id,
|
||||
gameId,
|
||||
linkedAccountId: owner.linkedAccountId,
|
||||
tier: 'lg'
|
||||
});
|
||||
expect(session.tier).toBe('lg');
|
||||
|
||||
const jobs = await Session.listJobsForMachine(machineId);
|
||||
expect(jobs[0]!.kind).toBe('session.start');
|
||||
expect(jobs[0]!.boxTier).toBe('lg');
|
||||
});
|
||||
|
||||
test('a job belongs to the machine its box is placed on and to no other', async () => {
|
||||
const mine = await scene('ses-job-mine', 5411);
|
||||
const theirs = await scene('ses-job-theirs', 5412);
|
||||
|
||||
Reference in New Issue
Block a user