feat: Add Session

This commit is contained in:
Wanjohi
2025-10-27 11:38:05 +03:00
parent 13cb3f15a7
commit 3a08e27614
6 changed files with 274 additions and 1554 deletions

View File

@@ -44,6 +44,45 @@ export namespace Examples {
export const Session = {
id: Id("session"),
description: "A gaming session for streaming gameplay",
retry: {
maxAttempts: 3,
factor: 2,
minTimeoutInMs: 1000,
maxTimeoutInMs: 30000,
randomize: true,
},
queue: {
name: "gaming-queue",
concurrencyLimit: 5,
},
machine: "ns3-large" as const,
maxDuration: 3600,
relayUrl: "https://relay.nestri.io",
resolution: "1920x1080" as const,
framerate: 60,
room: "game-room-123",
gpuVendor: "nvidia",
gpuName: "RTX 4090",
gpuIndex: 0,
gpuCardPath: "/dev/dri/renderD128",
videoCodec: "h264",
videoEncoder: "nvenc",
videoRateControl: "cbr" as const,
videoCqp: 26,
videoBitrate: 6000,
videoBitrateMax: 8000,
videoEncoderType: "hardware" as const,
videoBitDepth: 8 as const,
audioCaptureMethod: "pipewire" as const,
audioCodec: "opus",
audioEncoder: "opusenc",
audioRateControl: "cbr" as const,
audioBitrate: 128,
audioBitrateMax: 192,
zeroCopy: false,
verbose: false,
run: "async (payload, { ctx, signal }) => { /* session logic */ }",
};
export const App = {

View File

@@ -1,3 +1,202 @@
export * from "./json.js";
export * from "./types.js";
export * from "./schema.js";
import { z } from "zod/v4";
import { Identifier } from "../id.js";
import { Examples } from "../examples.js";
// Basic types we need
export const MachinePresetName = z.enum([
"ns3-micro",
"ns3-small",
"ns3-medium",
"ns3-large",
]);
export const ScreenResolutionPresetName = z.enum([
"1280x720",
"1080x1920",
"1440x2560",
"2160x3840",
]);
export const VideoCodecName = z.enum(["h264", "h265", "vp8", "vp9", "av1"]);
export const AudioCodecName = z.enum(["opus", "aac", "mp3", "pcm"]);
export const RetryOptions = z.object({
maxAttempts: z.number().int().optional(),
factor: z.number().optional(),
minTimeoutInMs: z.number().int().optional(),
maxTimeoutInMs: z.number().int().optional(),
randomize: z.boolean().optional(),
outOfMemory: z
.object({
machine: MachinePresetName.optional(),
})
.optional(),
});
export namespace Session {
export const Info = z
.object({
id: Identifier.schema("session").meta({
description: "The unique identifier of the session",
example: Examples.Session.id,
}),
description: z.string().optional().meta({
description: "Description of what the session does",
example: Examples.Session.description,
}),
retry: RetryOptions.optional().meta({
description: "Retry configuration for failed session attempts",
example: Examples.Session.retry,
}),
queue: z
.object({
name: z.string().optional(),
concurrencyLimit: z.number().optional(),
})
.optional()
.meta({
description: "Queue configuration for concurrent session execution",
example: Examples.Session.queue,
}),
machine: MachinePresetName.optional().meta({
description: "Machine preset for the session",
example: Examples.Session.machine,
}),
maxDuration: z.number().optional().meta({
description: "Maximum duration in seconds the session can run",
example: Examples.Session.maxDuration,
}),
relayUrl: z.url().optional().meta({
description: "Nestri relay URL for streaming and signaling",
example: Examples.Session.relayUrl,
}),
resolution: ScreenResolutionPresetName.optional().meta({
description: "Display or stream resolution in 'WxH' format",
example: Examples.Session.resolution,
}),
framerate: z.number().optional().meta({
description: "Display or stream framerate",
example: Examples.Session.framerate,
}),
room: z.string().optional().meta({
description: "Room name or identifier for multiplayer sessions",
example: Examples.Session.room,
}),
gpuVendor: z.string().optional().meta({
description: "Preferred GPU vendor",
example: Examples.Session.gpuVendor,
}),
gpuName: z.string().optional().meta({
description: "Preferred GPU name",
example: Examples.Session.gpuName,
}),
gpuIndex: z.number().optional().meta({
description: "Specific GPU index to use",
example: Examples.Session.gpuIndex,
}),
gpuCardPath: z.string().optional().meta({
description: "Force specific GPU card by /dev/dri path",
example: Examples.Session.gpuCardPath,
}),
videoCodec: VideoCodecName.optional().meta({
description: "Preferred video codec for streaming",
example: Examples.Session.videoCodec,
}),
videoEncoder: z.string().optional().meta({
description: "Override video encoder implementation",
example: Examples.Session.videoEncoder,
}),
videoRateControl: z
.enum(["cbr", "vbr", "cq", "lossless"])
.optional()
.meta({
description: "Video rate control method",
example: Examples.Session.videoRateControl,
}),
videoCqp: z.number().min(1).max(51).optional().meta({
description: "Constant Quantization Parameter for quality control",
example: Examples.Session.videoCqp,
}),
videoBitrate: z.number().min(1).optional().meta({
description: "Target video bitrate in kbps",
example: Examples.Session.videoBitrate,
}),
videoBitrateMax: z.number().min(1).optional().meta({
description: "Maximum video bitrate in kbps",
example: Examples.Session.videoBitrateMax,
}),
videoEncoderType: z.enum(["hardware", "software"]).optional().meta({
description: "Encoder type: hardware or software",
example: Examples.Session.videoEncoderType,
}),
videoBitDepth: z
.union([z.literal(8), z.literal(10)])
.optional()
.meta({
description: "Video bit depth (8 or 10)",
example: Examples.Session.videoBitDepth,
}),
audioCaptureMethod: z
.enum(["pipewire", "pulse", "alsa"])
.optional()
.meta({
description: "Audio capture method",
example: Examples.Session.audioCaptureMethod,
}),
audioCodec: AudioCodecName.optional().meta({
description: "Preferred audio codec for streaming",
example: Examples.Session.audioCodec,
}),
audioEncoder: z.string().optional().meta({
description: "Override audio encoder binary",
example: Examples.Session.audioEncoder,
}),
audioRateControl: z.enum(["cbr", "vbr"]).optional().meta({
description: "Audio rate control method",
example: Examples.Session.audioRateControl,
}),
audioBitrate: z.number().min(1).optional().meta({
description: "Target audio bitrate in kbps",
example: Examples.Session.audioBitrate,
}),
audioBitrateMax: z.number().optional().meta({
description: "Maximum audio bitrate in kbps",
example: Examples.Session.audioBitrateMax,
}),
zeroCopy: z.boolean().optional().meta({
description: "Whether to use DMA-BUF for zero-copy video pipelines",
example: Examples.Session.zeroCopy,
}),
verbose: z.boolean().optional().meta({
description: "Enable verbose output for debugging",
example: Examples.Session.verbose,
}),
})
.meta({
ref: "Session",
description:
"Represents a session configuration with streaming and execution options",
example: Examples.Session,
});
export type Info = z.infer<typeof Info>;
// Session interface that matches the SDK session object
export interface Session {
/** Optional description of the session */
description?: string;
/** Start a session without waiting for the result */
start: (options?: any, requestOptions?: any) => Promise<any>;
/** Start a session and wait for the result */
startAndWait: (options?: any) => any;
}
// Session configuration type that includes the run function
export type Config = Omit<Info, "id"> & {
/** The function that executes when the session is started */
run: (ctx: any) => Promise<any>;
};
}

View File

@@ -1,118 +0,0 @@
import { z } from "zod/v4";
const LiteralSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
type Literal = z.infer<typeof LiteralSchema>;
export type DeserializedJson =
| Literal
| { [key: string]: DeserializedJson }
| DeserializedJson[];
export const DeserializedJsonSchema: z.ZodType<DeserializedJson> = z.lazy(() =>
z.union([
LiteralSchema,
z.array(DeserializedJsonSchema),
z.record(z.string(), DeserializedJsonSchema),
]),
);
const SerializableSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.date(),
z.undefined(),
z.symbol(),
]);
type Serializable = z.infer<typeof SerializableSchema>;
export type SerializableJson =
| Serializable
| { [key: string]: SerializableJson }
| SerializableJson[];
export const SerializableJsonSchema: z.ZodType<SerializableJson> = z.lazy(() =>
z.union([
SerializableSchema,
z.array(SerializableJsonSchema),
z.record(z.string(), SerializableJsonSchema),
]),
);
/**
* JSON Schema type definition - compatible with JSON Schema Draft 7
* Based on the JSONSchema7 type from @types/json-schema but defined inline to avoid import issues
*/
export interface JSONSchema {
$id?: string;
$ref?: string;
$schema?: string;
$comment?: string;
type?: JSONSchemaType | JSONSchemaType[];
enum?: any[];
const?: any;
// Number/Integer validations
multipleOf?: number;
maximum?: number;
exclusiveMaximum?: number;
minimum?: number;
exclusiveMinimum?: number;
// String validations
maxLength?: number;
minLength?: number;
pattern?: string;
format?: string;
// Array validations
items?: JSONSchema | JSONSchema[];
additionalItems?: JSONSchema | boolean;
maxItems?: number;
minItems?: number;
uniqueItems?: boolean;
contains?: JSONSchema;
// Object validations
maxProperties?: number;
minProperties?: number;
required?: string[];
properties?: Record<string, JSONSchema>;
patternProperties?: Record<string, JSONSchema>;
additionalProperties?: JSONSchema | boolean;
dependencies?: Record<string, JSONSchema | string[]>;
propertyNames?: JSONSchema;
// Conditional schemas
if?: JSONSchema;
then?: JSONSchema;
else?: JSONSchema;
// Boolean logic
allOf?: JSONSchema[];
anyOf?: JSONSchema[];
oneOf?: JSONSchema[];
not?: JSONSchema;
// Metadata
title?: string;
description?: string;
default?: any;
readOnly?: boolean;
writeOnly?: boolean;
examples?: any[];
// Additional properties for extensibility
[key: string]: any;
}
export type JSONSchemaType =
| "string"
| "number"
| "integer"
| "boolean"
| "object"
| "array"
| "null";

View File

@@ -1,152 +0,0 @@
export type SchemaZodEsque<TInput, TParsedInput> = {
_input: TInput;
_output: TParsedInput;
};
export function isSchemaZodEsque<TInput, TParsedInput>(
schema: Schema,
): schema is SchemaZodEsque<TInput, TParsedInput> {
return (
typeof schema === "object" &&
"_def" in schema &&
"parse" in schema &&
"parseAsync" in schema &&
"safeParse" in schema
);
}
export type SchemaValibotEsque<TInput, TParsedInput> = {
schema: {
_types?: {
input: TInput;
output: TParsedInput;
};
};
};
export function isSchemaValibotEsque<TInput, TParsedInput>(
schema: Schema,
): schema is SchemaValibotEsque<TInput, TParsedInput> {
return typeof schema === "object" && "_types" in schema;
}
export type SchemaArkTypeEsque<TInput, TParsedInput> = {
inferIn: TInput;
infer: TParsedInput;
};
export function isSchemaArkTypeEsque<TInput, TParsedInput>(
schema: Schema,
): schema is SchemaArkTypeEsque<TInput, TParsedInput> {
return (
typeof schema === "object" && "_inferIn" in schema && "_infer" in schema
);
}
export type SchemaMyZodEsque<TInput> = {
parse: (input: any) => TInput;
};
export type SchemaSuperstructEsque<TInput> = {
create: (input: unknown) => TInput;
};
export type SchemaCustomValidatorEsque<TInput> = (
input: unknown,
) => Promise<TInput> | TInput;
export type SchemaYupEsque<TInput> = {
validateSync: (input: unknown) => TInput;
};
export type SchemaScaleEsque<TInput> = {
assert(value: unknown): asserts value is TInput;
};
export type SchemaWithoutInput<TInput> =
| SchemaCustomValidatorEsque<TInput>
| SchemaMyZodEsque<TInput>
| SchemaScaleEsque<TInput>
| SchemaSuperstructEsque<TInput>
| SchemaYupEsque<TInput>;
export type SchemaWithInputOutput<TInput, TParsedInput> =
| SchemaZodEsque<TInput, TParsedInput>
| SchemaValibotEsque<TInput, TParsedInput>
| SchemaArkTypeEsque<TInput, TParsedInput>;
export type Schema = SchemaWithInputOutput<any, any> | SchemaWithoutInput<any>;
export type inferSchema<TSchema extends Schema> =
TSchema extends SchemaWithInputOutput<infer $TIn, infer $TOut>
? {
in: $TIn;
out: $TOut;
}
: TSchema extends SchemaWithoutInput<infer $InOut>
? {
in: $InOut;
out: $InOut;
}
: never;
export type inferSchemaIn<
TSchema extends Schema | undefined,
TDefault = unknown,
> = TSchema extends Schema ? inferSchema<TSchema>["in"] : TDefault;
export type inferSchemaOut<
TSchema extends Schema | undefined,
TDefault = unknown,
> = TSchema extends Schema ? inferSchema<TSchema>["out"] : TDefault;
export type SchemaParseFn<TType> = (value: unknown) => Promise<TType> | TType;
export type AnySchemaParseFn = SchemaParseFn<any>;
export function getSchemaParseFn<TType>(
procedureParser: Schema,
): SchemaParseFn<TType> {
const parser = procedureParser as any;
if (typeof parser === "function" && typeof parser.assert === "function") {
// ParserArkTypeEsque - arktype schemas shouldn't be called as a function because they return a union type instead of throwing
return parser.assert.bind(parser);
}
if (typeof parser === "function") {
// ParserValibotEsque (>= v0.31.0)
// ParserCustomValidatorEsque
return parser;
}
if (typeof parser.parseAsync === "function") {
// ParserZodEsque
return parser.parseAsync.bind(parser);
}
if (typeof parser.parse === "function") {
// ParserZodEsque
// ParserValibotEsque (< v0.13.0)
return parser.parse.bind(parser);
}
if (typeof parser.validateSync === "function") {
// ParserYupEsque
return parser.validateSync.bind(parser);
}
if (typeof parser.create === "function") {
// ParserSuperstructEsque
return parser.create.bind(parser);
}
if (typeof parser.assert === "function") {
// ParserScaleEsque
return (value) => {
parser.assert(value);
return value as TType;
};
}
throw new Error("Could not find a validator fn");
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,217 +1,45 @@
import {
type Session,
type RunTypes,
type InitOutput,
type StartOptions,
type inferSchemaIn,
type SchemaParseFn,
type SessionSchema,
type SessionOptions,
type SessionWithSchema,
type SessionWithSchemaOptions,
type RunHandleFromTypes,
type AnyRunTypes,
type SessionOptionsWithSchema,
getSchemaParseFn,
SessionRunPromise,
} from "@nestri/core/session/index";
import { Identifier } from "@nestri/core/id";
import { Session } from "@nestri/core/session/index";
// Overload: when payloadSchema is provided, payload type should be any
export function createRun<
TIdentifier extends string,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params: SessionOptionsWithSchema<TIdentifier, TOutput, TInitOutput>,
): Session<TIdentifier, any, TOutput>;
// Overload: normal case without payloadSchema
export function createRun<
TIdentifier extends string,
TInput = void,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params: SessionOptions<TIdentifier, TInput, TOutput, TInitOutput>,
): Session<TIdentifier, TInput, TOutput>;
export function createRun<
TIdentifier extends string,
TInput = void,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params:
| SessionOptions<TIdentifier, TInput, TOutput, TInitOutput>
| SessionOptionsWithSchema<TIdentifier, TOutput, TInitOutput>,
): Session<TIdentifier, TInput, TOutput> | Session<TIdentifier, any, TOutput> {
const session: Session<TIdentifier, TInput, TOutput> = {
id: params.id,
description: params.description,
jsonSchema: params.jsonSchema,
start: async (payload, options) => {
return await run_internal<RunTypes<TIdentifier, TInput, TOutput>>(
"run()",
params.id,
// Simple session creation function using our clean Session types
export function createSession(config: Session.Config): Session.Session {
const session: Session.Session = {
description: config.description,
start: async (payload: any) => {
console.log(
`Starting session ${config.description} with payload:`,
payload,
undefined,
{
queue: params.queue?.name,
...options,
},
);
// TODO: Implement actual session start logic
return {
id: `run_${Date.now()}` as `run_${string}`,
sessionIdentifier: config.description,
publicAccessToken: "mock_token",
};
},
startAndWait: (payload, options) => {
return new SessionRunPromise<TIdentifier, TOutput>((resolve, reject) => {
triggerAndWait_internal<TIdentifier, TInput, TOutput>(
"triggerAndWait()",
params.id,
payload,
undefined,
{
queue: params.queue?.name,
...options,
},
)
.then((result) => {
resolve(result);
})
.catch((error) => {
reject(error);
startAndWait: (payload: any) => {
console.log(
`Starting session ${config.description} and waiting with payload:`,
payload,
);
// TODO: Implement actual session start and wait logic
return new Promise((resolve) => {
setTimeout(() => {
resolve({
ok: true,
id: `run_${Date.now()}` as any,
sessionIdentifier: config.description,
});
}, params.id);
}, 1000);
});
},
};
registerTaskLifecycleHooks(params.id, params);
resourceCatalog.registerTaskMetadata({
id: params.id,
description: params.description,
queue: params.queue,
retry: params.retry
? { ...defaultRetryOptions, ...params.retry }
: undefined,
machine:
typeof params.machine === "string"
? { preset: params.machine }
: params.machine,
maxDuration: params.maxDuration,
payloadSchema: params.jsonSchema,
fns: {
run: params.run,
},
});
const queue = params.queue;
if (queue && typeof queue.name === "string") {
resourceCatalog.registerQueueMetadata({
name: queue.name,
concurrencyLimit: queue.concurrencyLimit,
});
}
// @ts-expect-error
session[Symbol.for("nestri/session")] = true;
// Store the run function for later execution
(session as any)._runFunction = config.run;
return session;
}
export function createSchemaTask<
TIdentifier extends string,
TSchema extends SessionSchema | undefined = undefined,
TOutput = unknown,
TInitOutput extends InitOutput = any,
>(
params: SessionWithSchemaOptions<TIdentifier, TSchema, TOutput, TInitOutput>,
): SessionWithSchema<TIdentifier, TSchema, TOutput> {
const parsePayload = params.schema
? getSchemaParseFn<inferSchemaIn<TSchema>>(params.schema)
: undefined;
const session: SessionWithSchema<TIdentifier, TSchema, TOutput> = {
id: params.id,
description: params.description,
schema: params.schema,
start: async (payload, options, requestOptions) => {
return await run_internal<
RunTypes<TIdentifier, inferSchemaIn<TSchema>, TOutput>
>(
"run()",
params.id,
payload,
parsePayload,
{
queue: params.queue?.name,
...options,
},
requestOptions,
);
},
startAndWait: (payload, options) => {
return new SessionRunPromise<TIdentifier, TOutput>((resolve, reject) => {
triggerAndWait_internal<TIdentifier, inferSchemaIn<TSchema>, TOutput>(
"triggerAndWait()",
params.id,
payload,
parsePayload,
{
queue: params.queue?.name,
...options,
},
)
.then((result) => {
resolve(result);
})
.catch((error) => {
reject(error);
});
}, params.id);
},
};
registerTaskLifecycleHooks(params.id, params);
resourceCatalog.registerTaskMetadata({
id: params.id,
description: params.description,
queue: params.queue,
retry: params.retry
? { ...defaultRetryOptions, ...params.retry }
: undefined,
machine: { preset: params.machine },
maxDuration: params.maxDuration,
fns: {
run: params.run,
parsePayload,
},
schema: params.schema,
});
const queue = params.queue;
if (queue && typeof queue.name === "string") {
resourceCatalog.registerQueueMetadata({
name: queue.name,
concurrencyLimit: queue.concurrencyLimit,
});
}
// @ts-expect-error
session[Symbol.for("nestri/session")] = true;
return session;
}
export const session = createRun;
export const schemaSession = createSchemaTask;
async function run_internal<TRunTypes extends AnyRunTypes>(
name: string,
id: TRunTypes["sessionIdentifier"],
payload: TRunTypes["payload"],
parsePayload?: SchemaParseFn<TRunTypes["payload"]>,
options?: StartOptions,
requestOptions?: any, //TriggerApiRequestOptions,
): Promise<RunHandleFromTypes<TRunTypes>> {}
export const session = createSession;