chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@internal/compute",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"main": "./dist/src/index.js",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"@triggerdotdev/source": "./src/index.ts",
|
||||
"import": "./dist/src/index.js",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"default": "./dist/src/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "6.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "pnpm run clean && tsc -p tsconfig.build.json",
|
||||
"dev": "tsc --watch -p tsconfig.build.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import type {
|
||||
TemplateCreateRequest,
|
||||
TemplateCreateResponse,
|
||||
InstanceCreateRequest,
|
||||
InstanceCreateResponse,
|
||||
InstanceSnapshotRequest,
|
||||
SnapshotRestoreRequest,
|
||||
} from "./types.js";
|
||||
|
||||
export type ComputeClientOptions = {
|
||||
gatewayUrl: string;
|
||||
authToken?: string;
|
||||
timeoutMs: number;
|
||||
/**
|
||||
* Called once per outbound request to collect cross-service correlation
|
||||
* headers (e.g. `traceparent`, `x-request-id`) from the caller's current
|
||||
* scope. The returned record is merged onto the outbound headers. Return
|
||||
* `{}` (or omit the option) to skip propagation.
|
||||
*/
|
||||
getPropagationHeaders?: () => Record<string, string>;
|
||||
};
|
||||
|
||||
export class ComputeClient {
|
||||
readonly templates: TemplatesNamespace;
|
||||
readonly instances: InstancesNamespace;
|
||||
readonly snapshots: SnapshotsNamespace;
|
||||
|
||||
constructor(private opts: ComputeClientOptions) {
|
||||
const http = new HttpTransport(opts);
|
||||
this.templates = new TemplatesNamespace(http);
|
||||
this.instances = new InstancesNamespace(http);
|
||||
this.snapshots = new SnapshotsNamespace(http);
|
||||
}
|
||||
}
|
||||
|
||||
// ── HTTP transport (shared plumbing) ─────────────────────────────────────────
|
||||
|
||||
type RequestOptions = {
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
class HttpTransport {
|
||||
constructor(private opts: ComputeClientOptions) {}
|
||||
|
||||
private get headers(): Record<string, string> {
|
||||
const h: Record<string, string> = { "Content-Type": "application/json" };
|
||||
if (this.opts.authToken) {
|
||||
h["Authorization"] = `Bearer ${this.opts.authToken}`;
|
||||
}
|
||||
const propagation = this.opts.getPropagationHeaders?.();
|
||||
if (propagation) {
|
||||
for (const [key, value] of Object.entries(propagation)) {
|
||||
if (value) {
|
||||
h[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
private signal(options?: RequestOptions): AbortSignal {
|
||||
return options?.signal ?? AbortSignal.timeout(this.opts.timeoutMs);
|
||||
}
|
||||
|
||||
async post<T = unknown>(
|
||||
path: string,
|
||||
body: unknown,
|
||||
options?: RequestOptions
|
||||
): Promise<T | undefined> {
|
||||
const url = `${this.opts.gatewayUrl}${path}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: this.signal(options),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text().catch(() => "unknown error");
|
||||
throw new ComputeClientError(response.status, errorBody, url);
|
||||
}
|
||||
|
||||
// 202 Accepted or 204 No Content - no body to parse
|
||||
if (response.status === 202 || response.status === 204) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
async delete(path: string, options?: RequestOptions): Promise<void> {
|
||||
const url = `${this.opts.gatewayUrl}${path}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: this.headers,
|
||||
signal: this.signal(options),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text().catch(() => "unknown error");
|
||||
throw new ComputeClientError(response.status, errorBody, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Error ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export class ComputeClientError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
public readonly body: string,
|
||||
public readonly url: string
|
||||
) {
|
||||
super(`Compute gateway request failed (${status}): ${body}`);
|
||||
this.name = "ComputeClientError";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Namespaces ───────────────────────────────────────────────────────────────
|
||||
|
||||
class TemplatesNamespace {
|
||||
constructor(private http: HttpTransport) {}
|
||||
|
||||
async create(
|
||||
req: TemplateCreateRequest,
|
||||
options?: RequestOptions
|
||||
): Promise<TemplateCreateResponse | undefined> {
|
||||
// Background mode returns 202 with no body; sync/callback mode returns
|
||||
// the full result. Caller decides whether to inspect.
|
||||
return this.http.post<TemplateCreateResponse>("/api/templates", req, options);
|
||||
}
|
||||
}
|
||||
|
||||
class InstancesNamespace {
|
||||
constructor(private http: HttpTransport) {}
|
||||
|
||||
async create(
|
||||
req: InstanceCreateRequest,
|
||||
options?: RequestOptions
|
||||
): Promise<InstanceCreateResponse> {
|
||||
const result = await this.http.post<InstanceCreateResponse>("/api/instances", req, options);
|
||||
if (!result) {
|
||||
throw new Error("Compute gateway returned no instance body");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async delete(runnerId: string, options?: RequestOptions): Promise<void> {
|
||||
return this.http.delete(`/api/instances/${runnerId}`, options);
|
||||
}
|
||||
|
||||
async snapshot(
|
||||
runnerId: string,
|
||||
req: InstanceSnapshotRequest,
|
||||
options?: RequestOptions
|
||||
): Promise<void> {
|
||||
await this.http.post(`/api/instances/${runnerId}/snapshot`, req, options);
|
||||
}
|
||||
}
|
||||
|
||||
class SnapshotsNamespace {
|
||||
constructor(private http: HttpTransport) {}
|
||||
|
||||
async restore(
|
||||
snapshotId: string,
|
||||
req: SnapshotRestoreRequest,
|
||||
options?: RequestOptions
|
||||
): Promise<void> {
|
||||
await this.http.post(`/api/snapshots/${snapshotId}/restore`, req, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Strip the digest suffix from a container image reference.
|
||||
* Tags are immutable, so we resolve by tag rather than pinning to a digest.
|
||||
*
|
||||
* "ghcr.io/org/image:tag@sha256:abc..." -> "ghcr.io/org/image:tag"
|
||||
* "ghcr.io/org/image@sha256:abc..." -> "ghcr.io/org/image"
|
||||
* "ghcr.io/org/image:tag" -> "ghcr.io/org/image:tag" (unchanged)
|
||||
*/
|
||||
export function stripImageDigest(imageRef: string): string {
|
||||
return imageRef.split("@")[0] ?? imageRef;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export { ComputeClient, ComputeClientError } from "./client.js";
|
||||
export type { ComputeClientOptions } from "./client.js";
|
||||
export { stripImageDigest } from "./imageRef.js";
|
||||
export {
|
||||
MachineConfigSchema,
|
||||
TemplateCreateRequestSchema,
|
||||
TemplateCreateResultEntrySchema,
|
||||
TemplateCreateResponseSchema,
|
||||
InstanceCreateRequestSchema,
|
||||
InstanceCreateResponseSchema,
|
||||
InstanceSnapshotRequestSchema,
|
||||
SnapshotRestoreRequestSchema,
|
||||
SnapshotCallbackPayloadSchema,
|
||||
} from "./types.js";
|
||||
export type {
|
||||
MachineConfig,
|
||||
TemplateCreateRequest,
|
||||
TemplateCreateResultEntry,
|
||||
TemplateCreateResponse,
|
||||
InstanceCreateRequest,
|
||||
InstanceCreateResponse,
|
||||
InstanceSnapshotRequest,
|
||||
SnapshotRestoreRequest,
|
||||
SnapshotCallbackPayload,
|
||||
} from "./types.js";
|
||||
@@ -0,0 +1,96 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// ── Templates ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const MachineConfigSchema = z.object({
|
||||
cpu: z.number(),
|
||||
memory_gb: z.number(),
|
||||
});
|
||||
export type MachineConfig = z.infer<typeof MachineConfigSchema>;
|
||||
|
||||
export const TemplateCreateRequestSchema = z.object({
|
||||
image: z.string(),
|
||||
machine_configs: z.array(MachineConfigSchema),
|
||||
background: z.boolean().optional(),
|
||||
callback: z
|
||||
.object({
|
||||
url: z.string(),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
export type TemplateCreateRequest = z.infer<typeof TemplateCreateRequestSchema>;
|
||||
|
||||
export const TemplateCreateResultEntrySchema = z.object({
|
||||
machine_config: MachineConfigSchema,
|
||||
error: z.string().optional(),
|
||||
});
|
||||
export type TemplateCreateResultEntry = z.infer<typeof TemplateCreateResultEntrySchema>;
|
||||
|
||||
export const TemplateCreateResponseSchema = z.object({
|
||||
results: z.array(TemplateCreateResultEntrySchema),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
export type TemplateCreateResponse = z.infer<typeof TemplateCreateResponseSchema>;
|
||||
|
||||
// ── Instances ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const InstanceCreateRequestSchema = z.object({
|
||||
name: z.string(),
|
||||
image: z.string(),
|
||||
env: z.record(z.string()),
|
||||
cpu: z.number(),
|
||||
memory_gb: z.number(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
// Per-instance identity labels; the provider promotes a configured subset
|
||||
// to network-policy selection. Distinct from metadata, which is
|
||||
// observability-only and never selected on.
|
||||
labels: z.record(z.string()).optional(),
|
||||
});
|
||||
export type InstanceCreateRequest = z.infer<typeof InstanceCreateRequestSchema>;
|
||||
|
||||
export const InstanceCreateResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
_timing: z.unknown().optional(),
|
||||
});
|
||||
export type InstanceCreateResponse = z.infer<typeof InstanceCreateResponseSchema>;
|
||||
|
||||
export const InstanceSnapshotRequestSchema = z.object({
|
||||
callback: z.object({
|
||||
url: z.string(),
|
||||
metadata: z.record(z.string()),
|
||||
}),
|
||||
});
|
||||
export type InstanceSnapshotRequest = z.infer<typeof InstanceSnapshotRequestSchema>;
|
||||
|
||||
// ── Snapshots ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const SnapshotRestoreRequestSchema = z.object({
|
||||
name: z.string(),
|
||||
metadata: z.record(z.string()),
|
||||
cpu: z.number(),
|
||||
memory_gb: z.number(),
|
||||
// Per-instance identity labels; the caller must resupply the same set as on
|
||||
// create. The provider doesn't persist them across a snapshot, so omitting
|
||||
// them drops the restored run's policy-based network selection.
|
||||
labels: z.record(z.string()).optional(),
|
||||
});
|
||||
export type SnapshotRestoreRequest = z.infer<typeof SnapshotRestoreRequestSchema>;
|
||||
|
||||
export const SnapshotCallbackPayloadSchema = z.discriminatedUnion("status", [
|
||||
z.object({
|
||||
status: z.literal("completed"),
|
||||
snapshot_id: z.string(),
|
||||
instance_id: z.string(),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
duration_ms: z.number().optional(),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal("failed"),
|
||||
instance_id: z.string(),
|
||||
error: z.string().optional(),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
duration_ms: z.number().optional(),
|
||||
}),
|
||||
]);
|
||||
export type SnapshotCallbackPayload = z.infer<typeof SnapshotCallbackPayloadSchema>;
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
||||
"outDir": "dist",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"moduleDetection": "force",
|
||||
"verbatimModuleSyntax": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"declaration": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"lib": ["ES2019", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"moduleDetection": "force",
|
||||
"verbatimModuleSyntax": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"strict": true
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user