Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions sdks/sandbox/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,24 @@
"gen:api": "node ./scripts/generate-api.mjs",
"build": "pnpm run gen:api && tsup",
"lint": "eslint src scripts --max-warnings 0",
"clean": "rm -rf dist"
"clean": "rm -rf dist",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"openapi-fetch": "^0.14.1",
"undici": "^7.18.2"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^9.39.2",
"globals": "^17.0.0",
"openapi-typescript": "^7.9.1",
"tsup": "^8.5.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.52.0"
"typescript-eslint": "^8.52.0",
"vitest": "^4.0.18"
}
}
161 changes: 161 additions & 0 deletions sdks/sandbox/javascript/src/adapters/poolsAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2025 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import createClient from "openapi-fetch";
import type { Client } from "openapi-fetch";

import { throwOnOpenApiFetchError } from "./openapiError.js";
import type { poolPaths, PoolComponents } from "../api/lifecycle.js";
import type { Pools } from "../services/pools.js";
import type {
CreatePoolRequest,
PoolCapacitySpec,
PoolInfo,
PoolListResponse,
PoolStatus,
UpdatePoolRequest,
} from "../models/pools.js";

type PoolClient = Client<poolPaths>;

type ApiPoolResponse = PoolComponents["schemas"]["PoolResponse"];
type ApiPoolCapacitySpec = PoolComponents["schemas"]["PoolCapacitySpec"];
type ApiPoolStatus = PoolComponents["schemas"]["PoolStatus"];

// ---- helpers ---------------------------------------------------------------

function parseOptionalDate(field: string, v: unknown): Date | undefined {
if (v === undefined || v === null) return undefined;
if (typeof v !== "string" || !v) {
throw new Error(`Invalid ${field}: expected ISO string, got ${typeof v}`);
}
const d = new Date(v);
if (Number.isNaN(d.getTime())) throw new Error(`Invalid ${field}: ${v}`);
return d;
}

function mapCapacitySpec(raw: ApiPoolCapacitySpec): PoolCapacitySpec {
return {
bufferMax: raw.bufferMax,
bufferMin: raw.bufferMin,
poolMax: raw.poolMax,
poolMin: raw.poolMin,
};
}

function mapPoolStatus(raw: ApiPoolStatus): PoolStatus {
return {
total: raw.total,
allocated: raw.allocated,
available: raw.available,
revision: raw.revision,
};
}

function mapPoolInfo(raw: ApiPoolResponse): PoolInfo {
return {
name: raw.name,
capacitySpec: mapCapacitySpec(raw.capacitySpec),
status: raw.status ? mapPoolStatus(raw.status) : undefined,
createdAt: parseOptionalDate("createdAt", raw.createdAt),
};
}

// ---- adapter ---------------------------------------------------------------

/**
* HTTP adapter implementing the {@link Pools} service interface.
*
* Uses an `openapi-fetch` client typed against the pool path definitions in
* `api/lifecycle.ts` to ensure the request/response shapes stay in sync.
*/
export class PoolsAdapter implements Pools {
private readonly client: PoolClient;

constructor(opts: {
baseUrl?: string;
apiKey?: string;
headers?: Record<string, string>;
fetch?: typeof fetch;
}) {
const headers: Record<string, string> = { ...(opts.headers ?? {}) };
if (opts.apiKey && !headers["OPEN-SANDBOX-API-KEY"]) {
headers["OPEN-SANDBOX-API-KEY"] = opts.apiKey;
}

const createClientFn =
(createClient as unknown as { default?: typeof createClient }).default ??
createClient;
this.client = createClientFn<poolPaths>({
baseUrl: opts.baseUrl ?? "http://localhost:8080/v1",
headers,
fetch: opts.fetch,
});
}

async createPool(req: CreatePoolRequest): Promise<PoolInfo> {
const { data, error, response } = await this.client.POST("/pools", {
body: req as PoolComponents["schemas"]["CreatePoolRequest"],
});
throwOnOpenApiFetchError({ error, response }, "Create pool failed");
const raw = data as ApiPoolResponse | undefined;
if (!raw || typeof raw !== "object") {
throw new Error("Create pool failed: unexpected response shape");
}
return mapPoolInfo(raw);
}

async getPool(poolName: string): Promise<PoolInfo> {
const { data, error, response } = await this.client.GET("/pools/{poolName}", {
params: { path: { poolName } },
});
throwOnOpenApiFetchError({ error, response }, `Get pool '${poolName}' failed`);
const raw = data as ApiPoolResponse | undefined;
if (!raw || typeof raw !== "object") {
throw new Error(`Get pool '${poolName}' failed: unexpected response shape`);
}
return mapPoolInfo(raw);
}

async listPools(): Promise<PoolListResponse> {
const { data, error, response } = await this.client.GET("/pools", {});
throwOnOpenApiFetchError({ error, response }, "List pools failed");
const raw = data as PoolComponents["schemas"]["ListPoolsResponse"] | undefined;
if (!raw || typeof raw !== "object") {
throw new Error("List pools failed: unexpected response shape");
}
const items = Array.isArray(raw.items) ? raw.items.map(mapPoolInfo) : [];
return { items };
}

async updatePool(poolName: string, req: UpdatePoolRequest): Promise<PoolInfo> {
const { data, error, response } = await this.client.PUT("/pools/{poolName}", {
params: { path: { poolName } },
body: req as PoolComponents["schemas"]["UpdatePoolRequest"],
});
throwOnOpenApiFetchError({ error, response }, `Update pool '${poolName}' failed`);
const raw = data as ApiPoolResponse | undefined;
if (!raw || typeof raw !== "object") {
throw new Error(`Update pool '${poolName}' failed: unexpected response shape`);
}
return mapPoolInfo(raw);
}

async deletePool(poolName: string): Promise<void> {
const { error, response } = await this.client.DELETE("/pools/{poolName}", {
params: { path: { poolName } },
});
throwOnOpenApiFetchError({ error, response }, `Delete pool '${poolName}' failed`);
}
}
125 changes: 125 additions & 0 deletions sdks/sandbox/javascript/src/api/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -901,3 +901,128 @@ export interface components {
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;

// ============================================================================
// Pool paths and components (manually added – not auto-generated)
// ============================================================================

export interface PoolComponents {
schemas: {
PoolCapacitySpec: {
bufferMax: number;
bufferMin: number;
poolMax: number;
poolMin: number;
};
PoolStatus: {
total: number;
allocated: number;
available: number;
revision: string;
};
PoolResponse: {
name: string;
capacitySpec: PoolComponents["schemas"]["PoolCapacitySpec"];
status?: PoolComponents["schemas"]["PoolStatus"];
/** @description ISO 8601 timestamp */
createdAt?: string;
};
ListPoolsResponse: {
items: PoolComponents["schemas"]["PoolResponse"][];
};
CreatePoolRequest: {
name: string;
template: Record<string, unknown>;
capacitySpec: PoolComponents["schemas"]["PoolCapacitySpec"];
};
UpdatePoolRequest: {
capacitySpec: PoolComponents["schemas"]["PoolCapacitySpec"];
};
};
}

export interface poolPaths {
"/pools": {
parameters: { query?: never; header?: never; path?: never; cookie?: never };
/** List all pre-warmed resource pools */
get: {
parameters: { query?: never; header?: never; path?: never; cookie?: never };
requestBody?: never;
responses: {
200: { content: { "application/json": PoolComponents["schemas"]["ListPoolsResponse"] } };
401: components["responses"]["Unauthorized"];
500: components["responses"]["InternalServerError"];
501: { content: { "application/json": components["schemas"]["ErrorResponse"] } };
};
};
put?: never;
/** Create a pre-warmed resource pool */
post: {
parameters: { query?: never; header?: never; path?: never; cookie?: never };
requestBody: { content: { "application/json": PoolComponents["schemas"]["CreatePoolRequest"] } };
responses: {
201: { content: { "application/json": PoolComponents["schemas"]["PoolResponse"] } };
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
409: components["responses"]["Conflict"];
500: components["responses"]["InternalServerError"];
501: { content: { "application/json": components["schemas"]["ErrorResponse"] } };
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/pools/{poolName}": {
parameters: {
query?: never;
header?: never;
path: { poolName: string };
cookie?: never;
};
/** Retrieve a pool by name */
get: {
parameters: { query?: never; header?: never; path: { poolName: string }; cookie?: never };
requestBody?: never;
responses: {
200: { content: { "application/json": PoolComponents["schemas"]["PoolResponse"] } };
401: components["responses"]["Unauthorized"];
404: components["responses"]["NotFound"];
500: components["responses"]["InternalServerError"];
501: { content: { "application/json": components["schemas"]["ErrorResponse"] } };
};
};
/** Update pool capacity configuration */
put: {
parameters: { query?: never; header?: never; path: { poolName: string }; cookie?: never };
requestBody: { content: { "application/json": PoolComponents["schemas"]["UpdatePoolRequest"] } };
responses: {
200: { content: { "application/json": PoolComponents["schemas"]["PoolResponse"] } };
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
404: components["responses"]["NotFound"];
500: components["responses"]["InternalServerError"];
501: { content: { "application/json": components["schemas"]["ErrorResponse"] } };
};
};
post?: never;
/** Delete a pool */
delete: {
parameters: { query?: never; header?: never; path: { poolName: string }; cookie?: never };
requestBody?: never;
responses: {
204: { content?: never };
401: components["responses"]["Unauthorized"];
404: components["responses"]["NotFound"];
500: components["responses"]["InternalServerError"];
501: { content: { "application/json": components["schemas"]["ErrorResponse"] } };
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
2 changes: 2 additions & 0 deletions sdks/sandbox/javascript/src/factory/adapterFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { ExecdCommands } from "../services/execdCommands.js";
import type { ExecdHealth } from "../services/execdHealth.js";
import type { ExecdMetrics } from "../services/execdMetrics.js";
import type { Sandboxes } from "../services/sandboxes.js";
import type { Pools } from "../services/pools.js";

export interface CreateLifecycleStackOptions {
connectionConfig: ConnectionConfig;
Expand All @@ -26,6 +27,7 @@ export interface CreateLifecycleStackOptions {

export interface LifecycleStack {
sandboxes: Sandboxes;
pools: Pools;
}

export interface CreateExecdStackOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CommandsAdapter } from "../adapters/commandsAdapter.js";
import { FilesystemAdapter } from "../adapters/filesystemAdapter.js";
import { HealthAdapter } from "../adapters/healthAdapter.js";
import { MetricsAdapter } from "../adapters/metricsAdapter.js";
import { PoolsAdapter } from "../adapters/poolsAdapter.js";
import { SandboxesAdapter } from "../adapters/sandboxesAdapter.js";

import type { AdapterFactory, CreateExecdStackOptions, CreateLifecycleStackOptions, ExecdStack, LifecycleStack } from "./adapterFactory.js";
Expand All @@ -32,7 +33,13 @@ export class DefaultAdapterFactory implements AdapterFactory {
fetch: opts.connectionConfig.fetch,
});
const sandboxes = new SandboxesAdapter(lifecycleClient);
return { sandboxes };
const pools = new PoolsAdapter({
baseUrl: opts.lifecycleBaseUrl,
apiKey: opts.connectionConfig.apiKey,
headers: opts.connectionConfig.headers,
fetch: opts.connectionConfig.fetch,
});
return { sandboxes, pools };
}

createExecdStack(opts: CreateExecdStackOptions): ExecdStack {
Expand Down
18 changes: 17 additions & 1 deletion sdks/sandbox/javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,20 @@ export type {
SetPermissionEntry,
WriteEntry,
} from "./models/filesystem.js";
export type { SandboxFiles } from "./services/filesystem.js";
export type { SandboxFiles } from "./services/filesystem.js";

// Pool management
export type {
CreatePoolRequest,
PoolCapacitySpec,
PoolInfo,
PoolListResponse,
PoolStatus,
UpdatePoolRequest,
} from "./models/pools.js";
export type { Pools } from "./services/pools.js";
export { PoolManager } from "./poolManager.js";
export type { PoolManagerOptions } from "./poolManager.js";

// Pool management – synchronous (Node.js only)
export { PoolManagerSync } from "./poolManagerSync.js";
Loading