freestyle-sandboxes 0.1.42 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs CHANGED
@@ -3763,6 +3763,9 @@ class ApiClient {
3763
3763
  };
3764
3764
  return this.fetchFn(url, finalOptions);
3765
3765
  }
3766
+ resolveUrl(path, params, query) {
3767
+ return this.buildUrl(path, params, query);
3768
+ }
3766
3769
  getRaw(path, options) {
3767
3770
  const url = this.buildUrl(path, options?.params, options?.query);
3768
3771
  return this.requestRaw("GET", url, void 0, options?.headers);
@@ -3777,6 +3780,11 @@ class ApiClient {
3777
3780
  const url = this.buildUrl(path, options?.params, options?.query);
3778
3781
  return this.request("POST", url, options?.body, options?.headers);
3779
3782
  }
3783
+ postRaw(path, ...args) {
3784
+ const options = args[0];
3785
+ const url = this.buildUrl(path, options?.params, options?.query);
3786
+ return this.requestRaw("POST", url, options?.body, options?.headers);
3787
+ }
3780
3788
  put(path, ...args) {
3781
3789
  const options = args[0];
3782
3790
  const url = this.buildUrl(path, options?.params, options?.query);
@@ -6013,6 +6021,94 @@ function composeVmSpecs(specs) {
6013
6021
  const DEFAULT_CONFIGURE_BASE_IMAGE = "FROM debian:trixie-slim";
6014
6022
  const RUN_COMMANDS_SYSTEMD_SERVICE_PREFIX = "freestyle-run-command";
6015
6023
  const WAIT_FOR_SYSTEMD_SERVICE_PREFIX = "freestyle-wait-for";
6024
+ const BACKGROUND_AFTER_SECS_HEADER = "x-freestyle-background-after-secs";
6025
+ const BACKGROUND_REQUEST_ID_HEADER = "x-freestyle-background-request-id";
6026
+ const DEFAULT_BACKGROUND_AFTER_SECS = 5;
6027
+ const DEFAULT_BACKGROUND_POLL_INTERVAL_MS = 2e3;
6028
+ function delay(ms) {
6029
+ const timerApi = globalThis;
6030
+ return new Promise((resolve) => timerApi.setTimeout(resolve, ms));
6031
+ }
6032
+ function extractBackgroundRequestId(response, body) {
6033
+ return response.headers.get(BACKGROUND_REQUEST_ID_HEADER) ?? body?.requestId ?? body?.request_id;
6034
+ }
6035
+ async function parseJsonResponse(response) {
6036
+ return await response.json();
6037
+ }
6038
+ async function readResponseError(response, fallbackMessage) {
6039
+ const responseText = await response.text();
6040
+ if (!responseText) {
6041
+ return fallbackMessage;
6042
+ }
6043
+ try {
6044
+ const errorBody = JSON.parse(responseText);
6045
+ return errorBody.message ?? errorBody.error ?? responseText;
6046
+ } catch {
6047
+ return responseText;
6048
+ }
6049
+ }
6050
+ async function emitBackgroundLogs(apiClient, requestId, logger, seenLogs) {
6051
+ const response = await apiClient.fetch(
6052
+ apiClient.resolveUrl("/observability/v1/logs", void 0, { requestId }),
6053
+ { method: "GET" }
6054
+ );
6055
+ if (!response.ok) {
6056
+ return;
6057
+ }
6058
+ const payload = await response.json();
6059
+ for (const entry of payload.logs ?? []) {
6060
+ const key = `${entry.timestamp} ${entry.message}`;
6061
+ if (seenLogs.has(key)) {
6062
+ continue;
6063
+ }
6064
+ seenLogs.add(key);
6065
+ logger(`[${entry.timestamp}] ${entry.message}`);
6066
+ }
6067
+ }
6068
+ async function waitForBackgroundRequest(apiClient, requestId, logger) {
6069
+ const seenLogs = /* @__PURE__ */ new Set();
6070
+ const resultUrl = apiClient.resolveUrl(
6071
+ `/auth/v1/background-requests/${encodeURIComponent(requestId)}`
6072
+ );
6073
+ while (true) {
6074
+ await emitBackgroundLogs(apiClient, requestId, logger, seenLogs);
6075
+ const response = await apiClient.fetch(resultUrl, { method: "GET" });
6076
+ if (response.status === 202) {
6077
+ await delay(DEFAULT_BACKGROUND_POLL_INTERVAL_MS);
6078
+ continue;
6079
+ }
6080
+ if (!response.ok) {
6081
+ const message = await readResponseError(
6082
+ response,
6083
+ `Background request ${requestId} failed`
6084
+ );
6085
+ throw new Error(message);
6086
+ }
6087
+ return parseJsonResponse(response);
6088
+ }
6089
+ }
6090
+ async function postWithBackgroundLogger(apiClient, path, options, logger) {
6091
+ const response = await apiClient.postRaw(path, {
6092
+ ...options,
6093
+ headers: logger ? {
6094
+ ...options.headers ?? {},
6095
+ [BACKGROUND_AFTER_SECS_HEADER]: String(DEFAULT_BACKGROUND_AFTER_SECS)
6096
+ } : options.headers
6097
+ });
6098
+ if (response.status !== 202 || !logger) {
6099
+ return parseJsonResponse(response);
6100
+ }
6101
+ const accepted = await parseJsonResponse(
6102
+ response
6103
+ );
6104
+ const requestId = extractBackgroundRequestId(response, accepted);
6105
+ if (!requestId) {
6106
+ throw new Error(
6107
+ `Background request response for ${path} did not include a request ID`
6108
+ );
6109
+ }
6110
+ return waitForBackgroundRequest(apiClient, requestId, logger);
6111
+ }
6016
6112
  function escapeRegExp(value) {
6017
6113
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6018
6114
  }
@@ -6394,9 +6490,9 @@ class Vm {
6394
6490
  ref({ vmId }) {
6395
6491
  return new Vm({ vmId, freestyle: this._freestyle });
6396
6492
  }
6397
- async delete({ vmId }) {
6493
+ async delete() {
6398
6494
  return this.apiClient.delete("/v1/vms/{vm_id}", {
6399
- params: { vm_id: vmId }
6495
+ params: { vm_id: this.vmId }
6400
6496
  });
6401
6497
  }
6402
6498
  }
@@ -6886,9 +6982,15 @@ class VmsNamespace {
6886
6982
  }
6887
6983
  snapshots;
6888
6984
  async create(options = {}) {
6985
+ let logger;
6889
6986
  if (isVmSpecLike$1(options)) {
6890
6987
  options = { spec: cloneVmSpecTree(options) };
6891
6988
  } else {
6989
+ logger = options.logger;
6990
+ if ("logger" in options) {
6991
+ const { logger: _logger, ...rest } = options;
6992
+ options = rest;
6993
+ }
6892
6994
  if (isVmSpecLike$1(options.spec)) {
6893
6995
  options.spec = cloneVmSpecTree(options.spec);
6894
6996
  }
@@ -6968,7 +7070,8 @@ class VmsNamespace {
6968
7070
  if (isVmTemplateLike$1(config.template)) {
6969
7071
  config.template = await ensureNestedTemplates(
6970
7072
  config.template,
6971
- this.snapshots
7073
+ this.snapshots,
7074
+ logger
6972
7075
  );
6973
7076
  }
6974
7077
  const keys = Object.keys(builders);
@@ -7009,27 +7112,24 @@ class VmsNamespace {
7009
7112
  template: _template,
7010
7113
  ...requestConfig
7011
7114
  } = config;
7012
- let slowPathTimeout;
7013
- slowPathTimeout = setTimeout(() => {
7014
- console.log(
7015
- "VM creation is taking longer than expected. This usually happens when there's a cache miss on your vm's base snapshot. Subsequent vm creations with this configuration will likely be much faster."
7016
- );
7017
- }, 5e3);
7018
- const response = await this.freestyle._apiClient.post("/v1/vms", {
7019
- body: {
7020
- ...requestConfig,
7021
- template: normalizedRequestTemplate,
7022
- // Cast systemd since we've processed SystemdServiceInput[] to RawSystemdService[]
7023
- systemd: config.systemd,
7024
- // Normalize git options - default config to {}
7025
- git: normalizeGitOptions(config.git)
7026
- }
7027
- }).catch((e) => {
7028
- if (slowPathTimeout) clearTimeout(slowPathTimeout);
7115
+ const response = await postWithBackgroundLogger(
7116
+ this.freestyle._apiClient,
7117
+ "/v1/vms",
7118
+ {
7119
+ body: {
7120
+ ...requestConfig,
7121
+ template: normalizedRequestTemplate,
7122
+ // Cast systemd since we've processed SystemdServiceInput[] to RawSystemdService[]
7123
+ systemd: config.systemd,
7124
+ // Normalize git options - default config to {}
7125
+ git: normalizeGitOptions(config.git)
7126
+ }
7127
+ },
7128
+ logger
7129
+ ).catch((e) => {
7029
7130
  enhanceError(e);
7030
7131
  throw e;
7031
7132
  });
7032
- if (slowPathTimeout) clearTimeout(slowPathTimeout);
7033
7133
  const vmId = response.id;
7034
7134
  const vm = new Vm({ vmId, freestyle: this.freestyle });
7035
7135
  for (const key in builders) {
@@ -7182,11 +7282,12 @@ class VmSnapshotsNamespace {
7182
7282
  this.apiClient = apiClient;
7183
7283
  }
7184
7284
  async ensure(options) {
7285
+ const { logger, ...restOptions } = options;
7185
7286
  let requestOptions = {
7186
- ...options,
7187
- spec: isVmSpecLike$1(options.spec) ? cloneVmSpecTree(options.spec) : void 0,
7188
- snapshot: isVmSpecLike$1(options.snapshot) ? cloneVmSpecTree(options.snapshot) : void 0,
7189
- template: isVmTemplateLike$1(options.template) ? cloneVmTemplateTree(options.template) : options.template
7287
+ ...restOptions,
7288
+ spec: isVmSpecLike$1(restOptions.spec) ? cloneVmSpecTree(restOptions.spec) : void 0,
7289
+ snapshot: isVmSpecLike$1(restOptions.snapshot) ? cloneVmSpecTree(restOptions.snapshot) : void 0,
7290
+ template: isVmTemplateLike$1(restOptions.template) ? cloneVmTemplateTree(restOptions.template) : restOptions.template
7190
7291
  };
7191
7292
  if (isVmSpecLike$1(requestOptions.snapshot)) {
7192
7293
  if (isVmSpecLike$1(requestOptions.spec)) {
@@ -7224,7 +7325,8 @@ class VmSnapshotsNamespace {
7224
7325
  if (isVmTemplateLike$1(processedTemplate.raw.template)) {
7225
7326
  requestOptions.template = await ensureNestedTemplates(
7226
7327
  processedTemplate,
7227
- this
7328
+ this,
7329
+ logger
7228
7330
  );
7229
7331
  }
7230
7332
  }
@@ -7233,27 +7335,43 @@ class VmSnapshotsNamespace {
7233
7335
  "snapshots.ensure requires a template or spec to build a snapshot"
7234
7336
  );
7235
7337
  }
7236
- const startTime = Date.now();
7237
- return this.apiClient.post("/v1/vms/snapshots", {
7238
- body: {
7239
- ...requestOptions,
7240
- template: normalizeTemplateForRequest(
7241
- isVmTemplateLike$1(requestOptions.template) ? requestOptions.template.raw : requestOptions.template
7242
- )
7243
- }
7244
- }).then((response) => {
7245
- const elapsedTime = Date.now() - startTime;
7246
- if (elapsedTime > 5e3) {
7247
- console.log(
7248
- "Snapshot creation took longer than expected (> 5 seconds). This is slower because caches weren't available, but subsequent operations will be much faster."
7249
- );
7250
- }
7251
- return response;
7252
- }).catch((e) => {
7338
+ return postWithBackgroundLogger(
7339
+ this.apiClient,
7340
+ "/v1/vms/snapshots",
7341
+ {
7342
+ body: {
7343
+ ...requestOptions,
7344
+ template: normalizeTemplateForRequest(
7345
+ isVmTemplateLike$1(requestOptions.template) ? requestOptions.template.raw : requestOptions.template
7346
+ )
7347
+ }
7348
+ },
7349
+ logger
7350
+ ).catch((e) => {
7253
7351
  enhanceError(e);
7254
7352
  throw e;
7255
7353
  });
7256
7354
  }
7355
+ async create(options) {
7356
+ return this.ensure(options);
7357
+ }
7358
+ async delete({ snapshotId }) {
7359
+ const path = "/v1/vms/snapshots/{snapshot_id}";
7360
+ const response = await this.apiClient.fetch(
7361
+ this.apiClient.resolveUrl(path, { snapshot_id: snapshotId }),
7362
+ { method: "DELETE" }
7363
+ );
7364
+ if (!response.ok) {
7365
+ const errorBody = await response.json().catch(() => null);
7366
+ if (errorBody?.code) {
7367
+ throw errorFromJSON(errorBody);
7368
+ }
7369
+ throw new Error(
7370
+ `Failed to delete snapshot: ${response.status} ${response.statusText}`
7371
+ );
7372
+ }
7373
+ return await response.json();
7374
+ }
7257
7375
  }
7258
7376
  async function processTemplateTree(template) {
7259
7377
  if (template.raw.baseImage) {
@@ -7325,24 +7443,24 @@ async function processTemplateTree(template) {
7325
7443
  }
7326
7444
  return template;
7327
7445
  }
7328
- async function ensureNestedTemplates(template, snapshots) {
7446
+ async function ensureNestedTemplates(template, snapshots, logger) {
7329
7447
  const templates = [template];
7330
7448
  while (isVmTemplateLike$1(templates.at(-1)?.raw.template)) {
7331
7449
  const innerTemplate = templates.at(-1).raw.template;
7332
7450
  templates.at(-1).raw.template = void 0;
7333
7451
  templates.push(innerTemplate);
7334
7452
  }
7335
- return await layerTemplates(templates, snapshots);
7453
+ return await layerTemplates(templates, snapshots, logger);
7336
7454
  }
7337
- async function layerTemplates(templates, snapshots) {
7455
+ async function layerTemplates(templates, snapshots, logger) {
7338
7456
  if (templates.length === 1) {
7339
7457
  return templates[0];
7340
7458
  }
7341
- let lastSnapshotId = (await snapshots.ensure({ template: templates.pop() })).snapshotId;
7459
+ let lastSnapshotId = (await snapshots.ensure({ template: templates.pop(), logger })).snapshotId;
7342
7460
  while (templates.length > 1) {
7343
7461
  const template = templates.pop();
7344
7462
  template.raw.snapshotId = lastSnapshotId;
7345
- lastSnapshotId = (await snapshots.ensure({ template })).snapshotId;
7463
+ lastSnapshotId = (await snapshots.ensure({ template, logger })).snapshotId;
7346
7464
  }
7347
7465
  const outermost = templates.pop();
7348
7466
  outermost.raw.snapshotId = lastSnapshotId;
package/index.d.cts CHANGED
@@ -6707,9 +6707,11 @@ declare class ApiClient {
6707
6707
  private requestRaw;
6708
6708
  private request;
6709
6709
  fetch(url: string, options?: RequestInit): Promise<Response>;
6710
+ resolveUrl(path: string, params?: Record<string, string>, query?: Record<string, any>): string;
6710
6711
  getRaw<P extends keyof GetPathMap>(path: P, options?: GetPathMap[P]["options"]): Promise<Response>;
6711
6712
  get<P extends keyof GetPathMap>(path: P, ...args: GetPathMap[P]["options"] extends undefined ? [options?: GetPathMap[P]["options"]] : [options: GetPathMap[P]["options"]]): Promise<GetPathMap[P]["response"]>;
6712
6713
  post<P extends keyof PostPathMap>(path: P, ...args: PostPathMap[P]["options"] extends undefined ? [options?: PostPathMap[P]["options"]] : [options: PostPathMap[P]["options"]]): Promise<PostPathMap[P]["response"]>;
6714
+ postRaw<P extends keyof PostPathMap>(path: P, ...args: PostPathMap[P]["options"] extends undefined ? [options?: PostPathMap[P]["options"]] : [options: PostPathMap[P]["options"]]): Promise<Response>;
6713
6715
  put<P extends keyof PutPathMap>(path: P, ...args: PutPathMap[P]["options"] extends undefined ? [options?: PutPathMap[P]["options"]] : [options: PutPathMap[P]["options"]]): Promise<PutPathMap[P]["response"]>;
6714
6716
  delete<P extends keyof DeletePathMap>(path: P, ...args: DeletePathMap[P]["options"] extends undefined ? [options?: DeletePathMap[P]["options"]] : [options: DeletePathMap[P]["options"]]): Promise<DeletePathMap[P]["response"]>;
6715
6717
  patch<P extends keyof PatchPathMap>(path: P, ...args: PatchPathMap[P]["options"] extends undefined ? [options?: PatchPathMap[P]["options"]] : [options: PatchPathMap[P]["options"]]): Promise<PatchPathMap[P]["response"]>;
@@ -12313,6 +12315,16 @@ type SystemdServiceInput = Omit<RawSystemdService, "mode" | "exec"> & {
12313
12315
  type VmWaitForConfig = Omit<SystemdServiceInput, "mode" | "exec" | "bash" | "deleteAfterSuccess"> & {
12314
12316
  intervalSeconds?: number;
12315
12317
  };
12318
+ type BackgroundRequestLogger = (message: string) => void;
12319
+ type SnapshotCreateOptions<T extends Record<string, VmWithLike> = {}> = Omit<PostV1VmsSnapshotsRequestBody, "template"> & {
12320
+ template?: VmTemplate<T> | PostV1VmsSnapshotsRequestBody["template"];
12321
+ spec?: VmSpec<T>;
12322
+ snapshot?: VmSpec<T>;
12323
+ logger?: BackgroundRequestLogger;
12324
+ };
12325
+ type SnapshotDeleteResponse = {
12326
+ snapshotId: string;
12327
+ };
12316
12328
  /**
12317
12329
  * Terminal management operations for a VM.
12318
12330
  */
@@ -12446,9 +12458,7 @@ declare class Vm {
12446
12458
  ref({ vmId }: {
12447
12459
  vmId: string;
12448
12460
  }): Vm;
12449
- delete({ vmId }: {
12450
- vmId: string;
12451
- }): Promise<ResponseDeleteV1VmsVmId200>;
12461
+ delete(): Promise<ResponseDeleteV1VmsVmId200>;
12452
12462
  }
12453
12463
  /**
12454
12464
  * Git configuration with optional config (defaults to {})
@@ -12551,6 +12561,7 @@ declare class VmsNamespace {
12551
12561
  template?: VmTemplate<T>;
12552
12562
  spec?: VmSpec<T>;
12553
12563
  snapshot?: VmSpec<T>;
12564
+ logger?: BackgroundRequestLogger;
12554
12565
  }): Promise<Omit<ResponsePostV1Vms200, "consoleUrl"> & {
12555
12566
  vmId: string;
12556
12567
  vm: Vm & {
@@ -12594,11 +12605,11 @@ declare class VmsNamespace {
12594
12605
  declare class VmSnapshotsNamespace {
12595
12606
  private apiClient;
12596
12607
  constructor(apiClient: ApiClient);
12597
- ensure<T extends Record<string, VmWithLike>>(options: Omit<PostV1VmsSnapshotsRequestBody, "template"> & {
12598
- template?: VmTemplate<T> | PostV1VmsSnapshotsRequestBody["template"];
12599
- spec?: VmSpec<T>;
12600
- snapshot?: VmSpec<T>;
12601
- }): Promise<ResponsePostV1VmsSnapshots200>;
12608
+ ensure<T extends Record<string, VmWithLike>>(options: SnapshotCreateOptions<T>): Promise<ResponsePostV1VmsSnapshots200>;
12609
+ create<T extends Record<string, VmWithLike>>(options: SnapshotCreateOptions<T>): Promise<ResponsePostV1VmsSnapshots200>;
12610
+ delete({ snapshotId }: {
12611
+ snapshotId: string;
12612
+ }): Promise<SnapshotDeleteResponse>;
12602
12613
  }
12603
12614
  type CreateVmOptions = Omit<PostV1VmsRequestBody, "template" | "systemd" | "git"> & {
12604
12615
  rootfsSizeGb?: number | null;
@@ -12615,6 +12626,7 @@ type CreateVmOptions = Omit<PostV1VmsRequestBody, "template" | "systemd" | "git"
12615
12626
  git?: null | GitOptions;
12616
12627
  discriminator?: CreateSnapshotRequest["template"]["discriminator"];
12617
12628
  skipCache?: CreateSnapshotRequest["template"]["skipCache"];
12629
+ logger?: BackgroundRequestLogger;
12618
12630
  };
12619
12631
 
12620
12632
  type CronSchedule = {
@@ -12763,4 +12775,4 @@ declare class Freestyle {
12763
12775
  declare const freestyle: Freestyle;
12764
12776
 
12765
12777
  export { CronNamespace, Deployment, errors as Errors, FileSystem, Freestyle, GitRepo, Identity, requests as Requests, responses as Responses, Systemd, SystemdService, Vm, VmBaseImage, VmBuilder, VmService, VmSpec, VmTemplate, VmWith, VmWithInstance, debugCreateRequests, freestyle, readFiles };
12766
- export type { CreateVmOptions, CronSchedule, FreestyleOptions, SystemdServiceInput, VmWaitForConfig, VmWithDefaultFieldRecord };
12778
+ export type { BackgroundRequestLogger, CreateVmOptions, CronSchedule, FreestyleOptions, SystemdServiceInput, VmWaitForConfig, VmWithDefaultFieldRecord };
package/index.d.mts CHANGED
@@ -6707,9 +6707,11 @@ declare class ApiClient {
6707
6707
  private requestRaw;
6708
6708
  private request;
6709
6709
  fetch(url: string, options?: RequestInit): Promise<Response>;
6710
+ resolveUrl(path: string, params?: Record<string, string>, query?: Record<string, any>): string;
6710
6711
  getRaw<P extends keyof GetPathMap>(path: P, options?: GetPathMap[P]["options"]): Promise<Response>;
6711
6712
  get<P extends keyof GetPathMap>(path: P, ...args: GetPathMap[P]["options"] extends undefined ? [options?: GetPathMap[P]["options"]] : [options: GetPathMap[P]["options"]]): Promise<GetPathMap[P]["response"]>;
6712
6713
  post<P extends keyof PostPathMap>(path: P, ...args: PostPathMap[P]["options"] extends undefined ? [options?: PostPathMap[P]["options"]] : [options: PostPathMap[P]["options"]]): Promise<PostPathMap[P]["response"]>;
6714
+ postRaw<P extends keyof PostPathMap>(path: P, ...args: PostPathMap[P]["options"] extends undefined ? [options?: PostPathMap[P]["options"]] : [options: PostPathMap[P]["options"]]): Promise<Response>;
6713
6715
  put<P extends keyof PutPathMap>(path: P, ...args: PutPathMap[P]["options"] extends undefined ? [options?: PutPathMap[P]["options"]] : [options: PutPathMap[P]["options"]]): Promise<PutPathMap[P]["response"]>;
6714
6716
  delete<P extends keyof DeletePathMap>(path: P, ...args: DeletePathMap[P]["options"] extends undefined ? [options?: DeletePathMap[P]["options"]] : [options: DeletePathMap[P]["options"]]): Promise<DeletePathMap[P]["response"]>;
6715
6717
  patch<P extends keyof PatchPathMap>(path: P, ...args: PatchPathMap[P]["options"] extends undefined ? [options?: PatchPathMap[P]["options"]] : [options: PatchPathMap[P]["options"]]): Promise<PatchPathMap[P]["response"]>;
@@ -12313,6 +12315,16 @@ type SystemdServiceInput = Omit<RawSystemdService, "mode" | "exec"> & {
12313
12315
  type VmWaitForConfig = Omit<SystemdServiceInput, "mode" | "exec" | "bash" | "deleteAfterSuccess"> & {
12314
12316
  intervalSeconds?: number;
12315
12317
  };
12318
+ type BackgroundRequestLogger = (message: string) => void;
12319
+ type SnapshotCreateOptions<T extends Record<string, VmWithLike> = {}> = Omit<PostV1VmsSnapshotsRequestBody, "template"> & {
12320
+ template?: VmTemplate<T> | PostV1VmsSnapshotsRequestBody["template"];
12321
+ spec?: VmSpec<T>;
12322
+ snapshot?: VmSpec<T>;
12323
+ logger?: BackgroundRequestLogger;
12324
+ };
12325
+ type SnapshotDeleteResponse = {
12326
+ snapshotId: string;
12327
+ };
12316
12328
  /**
12317
12329
  * Terminal management operations for a VM.
12318
12330
  */
@@ -12446,9 +12458,7 @@ declare class Vm {
12446
12458
  ref({ vmId }: {
12447
12459
  vmId: string;
12448
12460
  }): Vm;
12449
- delete({ vmId }: {
12450
- vmId: string;
12451
- }): Promise<ResponseDeleteV1VmsVmId200>;
12461
+ delete(): Promise<ResponseDeleteV1VmsVmId200>;
12452
12462
  }
12453
12463
  /**
12454
12464
  * Git configuration with optional config (defaults to {})
@@ -12551,6 +12561,7 @@ declare class VmsNamespace {
12551
12561
  template?: VmTemplate<T>;
12552
12562
  spec?: VmSpec<T>;
12553
12563
  snapshot?: VmSpec<T>;
12564
+ logger?: BackgroundRequestLogger;
12554
12565
  }): Promise<Omit<ResponsePostV1Vms200, "consoleUrl"> & {
12555
12566
  vmId: string;
12556
12567
  vm: Vm & {
@@ -12594,11 +12605,11 @@ declare class VmsNamespace {
12594
12605
  declare class VmSnapshotsNamespace {
12595
12606
  private apiClient;
12596
12607
  constructor(apiClient: ApiClient);
12597
- ensure<T extends Record<string, VmWithLike>>(options: Omit<PostV1VmsSnapshotsRequestBody, "template"> & {
12598
- template?: VmTemplate<T> | PostV1VmsSnapshotsRequestBody["template"];
12599
- spec?: VmSpec<T>;
12600
- snapshot?: VmSpec<T>;
12601
- }): Promise<ResponsePostV1VmsSnapshots200>;
12608
+ ensure<T extends Record<string, VmWithLike>>(options: SnapshotCreateOptions<T>): Promise<ResponsePostV1VmsSnapshots200>;
12609
+ create<T extends Record<string, VmWithLike>>(options: SnapshotCreateOptions<T>): Promise<ResponsePostV1VmsSnapshots200>;
12610
+ delete({ snapshotId }: {
12611
+ snapshotId: string;
12612
+ }): Promise<SnapshotDeleteResponse>;
12602
12613
  }
12603
12614
  type CreateVmOptions = Omit<PostV1VmsRequestBody, "template" | "systemd" | "git"> & {
12604
12615
  rootfsSizeGb?: number | null;
@@ -12615,6 +12626,7 @@ type CreateVmOptions = Omit<PostV1VmsRequestBody, "template" | "systemd" | "git"
12615
12626
  git?: null | GitOptions;
12616
12627
  discriminator?: CreateSnapshotRequest["template"]["discriminator"];
12617
12628
  skipCache?: CreateSnapshotRequest["template"]["skipCache"];
12629
+ logger?: BackgroundRequestLogger;
12618
12630
  };
12619
12631
 
12620
12632
  type CronSchedule = {
@@ -12763,4 +12775,4 @@ declare class Freestyle {
12763
12775
  declare const freestyle: Freestyle;
12764
12776
 
12765
12777
  export { CronNamespace, Deployment, errors as Errors, FileSystem, Freestyle, GitRepo, Identity, requests as Requests, responses as Responses, Systemd, SystemdService, Vm, VmBaseImage, VmBuilder, VmService, VmSpec, VmTemplate, VmWith, VmWithInstance, debugCreateRequests, freestyle, readFiles };
12766
- export type { CreateVmOptions, CronSchedule, FreestyleOptions, SystemdServiceInput, VmWaitForConfig, VmWithDefaultFieldRecord };
12778
+ export type { BackgroundRequestLogger, CreateVmOptions, CronSchedule, FreestyleOptions, SystemdServiceInput, VmWaitForConfig, VmWithDefaultFieldRecord };
package/index.mjs CHANGED
@@ -3761,6 +3761,9 @@ class ApiClient {
3761
3761
  };
3762
3762
  return this.fetchFn(url, finalOptions);
3763
3763
  }
3764
+ resolveUrl(path, params, query) {
3765
+ return this.buildUrl(path, params, query);
3766
+ }
3764
3767
  getRaw(path, options) {
3765
3768
  const url = this.buildUrl(path, options?.params, options?.query);
3766
3769
  return this.requestRaw("GET", url, void 0, options?.headers);
@@ -3775,6 +3778,11 @@ class ApiClient {
3775
3778
  const url = this.buildUrl(path, options?.params, options?.query);
3776
3779
  return this.request("POST", url, options?.body, options?.headers);
3777
3780
  }
3781
+ postRaw(path, ...args) {
3782
+ const options = args[0];
3783
+ const url = this.buildUrl(path, options?.params, options?.query);
3784
+ return this.requestRaw("POST", url, options?.body, options?.headers);
3785
+ }
3778
3786
  put(path, ...args) {
3779
3787
  const options = args[0];
3780
3788
  const url = this.buildUrl(path, options?.params, options?.query);
@@ -6011,6 +6019,94 @@ function composeVmSpecs(specs) {
6011
6019
  const DEFAULT_CONFIGURE_BASE_IMAGE = "FROM debian:trixie-slim";
6012
6020
  const RUN_COMMANDS_SYSTEMD_SERVICE_PREFIX = "freestyle-run-command";
6013
6021
  const WAIT_FOR_SYSTEMD_SERVICE_PREFIX = "freestyle-wait-for";
6022
+ const BACKGROUND_AFTER_SECS_HEADER = "x-freestyle-background-after-secs";
6023
+ const BACKGROUND_REQUEST_ID_HEADER = "x-freestyle-background-request-id";
6024
+ const DEFAULT_BACKGROUND_AFTER_SECS = 5;
6025
+ const DEFAULT_BACKGROUND_POLL_INTERVAL_MS = 2e3;
6026
+ function delay(ms) {
6027
+ const timerApi = globalThis;
6028
+ return new Promise((resolve) => timerApi.setTimeout(resolve, ms));
6029
+ }
6030
+ function extractBackgroundRequestId(response, body) {
6031
+ return response.headers.get(BACKGROUND_REQUEST_ID_HEADER) ?? body?.requestId ?? body?.request_id;
6032
+ }
6033
+ async function parseJsonResponse(response) {
6034
+ return await response.json();
6035
+ }
6036
+ async function readResponseError(response, fallbackMessage) {
6037
+ const responseText = await response.text();
6038
+ if (!responseText) {
6039
+ return fallbackMessage;
6040
+ }
6041
+ try {
6042
+ const errorBody = JSON.parse(responseText);
6043
+ return errorBody.message ?? errorBody.error ?? responseText;
6044
+ } catch {
6045
+ return responseText;
6046
+ }
6047
+ }
6048
+ async function emitBackgroundLogs(apiClient, requestId, logger, seenLogs) {
6049
+ const response = await apiClient.fetch(
6050
+ apiClient.resolveUrl("/observability/v1/logs", void 0, { requestId }),
6051
+ { method: "GET" }
6052
+ );
6053
+ if (!response.ok) {
6054
+ return;
6055
+ }
6056
+ const payload = await response.json();
6057
+ for (const entry of payload.logs ?? []) {
6058
+ const key = `${entry.timestamp} ${entry.message}`;
6059
+ if (seenLogs.has(key)) {
6060
+ continue;
6061
+ }
6062
+ seenLogs.add(key);
6063
+ logger(`[${entry.timestamp}] ${entry.message}`);
6064
+ }
6065
+ }
6066
+ async function waitForBackgroundRequest(apiClient, requestId, logger) {
6067
+ const seenLogs = /* @__PURE__ */ new Set();
6068
+ const resultUrl = apiClient.resolveUrl(
6069
+ `/auth/v1/background-requests/${encodeURIComponent(requestId)}`
6070
+ );
6071
+ while (true) {
6072
+ await emitBackgroundLogs(apiClient, requestId, logger, seenLogs);
6073
+ const response = await apiClient.fetch(resultUrl, { method: "GET" });
6074
+ if (response.status === 202) {
6075
+ await delay(DEFAULT_BACKGROUND_POLL_INTERVAL_MS);
6076
+ continue;
6077
+ }
6078
+ if (!response.ok) {
6079
+ const message = await readResponseError(
6080
+ response,
6081
+ `Background request ${requestId} failed`
6082
+ );
6083
+ throw new Error(message);
6084
+ }
6085
+ return parseJsonResponse(response);
6086
+ }
6087
+ }
6088
+ async function postWithBackgroundLogger(apiClient, path, options, logger) {
6089
+ const response = await apiClient.postRaw(path, {
6090
+ ...options,
6091
+ headers: logger ? {
6092
+ ...options.headers ?? {},
6093
+ [BACKGROUND_AFTER_SECS_HEADER]: String(DEFAULT_BACKGROUND_AFTER_SECS)
6094
+ } : options.headers
6095
+ });
6096
+ if (response.status !== 202 || !logger) {
6097
+ return parseJsonResponse(response);
6098
+ }
6099
+ const accepted = await parseJsonResponse(
6100
+ response
6101
+ );
6102
+ const requestId = extractBackgroundRequestId(response, accepted);
6103
+ if (!requestId) {
6104
+ throw new Error(
6105
+ `Background request response for ${path} did not include a request ID`
6106
+ );
6107
+ }
6108
+ return waitForBackgroundRequest(apiClient, requestId, logger);
6109
+ }
6014
6110
  function escapeRegExp(value) {
6015
6111
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6016
6112
  }
@@ -6392,9 +6488,9 @@ class Vm {
6392
6488
  ref({ vmId }) {
6393
6489
  return new Vm({ vmId, freestyle: this._freestyle });
6394
6490
  }
6395
- async delete({ vmId }) {
6491
+ async delete() {
6396
6492
  return this.apiClient.delete("/v1/vms/{vm_id}", {
6397
- params: { vm_id: vmId }
6493
+ params: { vm_id: this.vmId }
6398
6494
  });
6399
6495
  }
6400
6496
  }
@@ -6884,9 +6980,15 @@ class VmsNamespace {
6884
6980
  }
6885
6981
  snapshots;
6886
6982
  async create(options = {}) {
6983
+ let logger;
6887
6984
  if (isVmSpecLike$1(options)) {
6888
6985
  options = { spec: cloneVmSpecTree(options) };
6889
6986
  } else {
6987
+ logger = options.logger;
6988
+ if ("logger" in options) {
6989
+ const { logger: _logger, ...rest } = options;
6990
+ options = rest;
6991
+ }
6890
6992
  if (isVmSpecLike$1(options.spec)) {
6891
6993
  options.spec = cloneVmSpecTree(options.spec);
6892
6994
  }
@@ -6966,7 +7068,8 @@ class VmsNamespace {
6966
7068
  if (isVmTemplateLike$1(config.template)) {
6967
7069
  config.template = await ensureNestedTemplates(
6968
7070
  config.template,
6969
- this.snapshots
7071
+ this.snapshots,
7072
+ logger
6970
7073
  );
6971
7074
  }
6972
7075
  const keys = Object.keys(builders);
@@ -7007,27 +7110,24 @@ class VmsNamespace {
7007
7110
  template: _template,
7008
7111
  ...requestConfig
7009
7112
  } = config;
7010
- let slowPathTimeout;
7011
- slowPathTimeout = setTimeout(() => {
7012
- console.log(
7013
- "VM creation is taking longer than expected. This usually happens when there's a cache miss on your vm's base snapshot. Subsequent vm creations with this configuration will likely be much faster."
7014
- );
7015
- }, 5e3);
7016
- const response = await this.freestyle._apiClient.post("/v1/vms", {
7017
- body: {
7018
- ...requestConfig,
7019
- template: normalizedRequestTemplate,
7020
- // Cast systemd since we've processed SystemdServiceInput[] to RawSystemdService[]
7021
- systemd: config.systemd,
7022
- // Normalize git options - default config to {}
7023
- git: normalizeGitOptions(config.git)
7024
- }
7025
- }).catch((e) => {
7026
- if (slowPathTimeout) clearTimeout(slowPathTimeout);
7113
+ const response = await postWithBackgroundLogger(
7114
+ this.freestyle._apiClient,
7115
+ "/v1/vms",
7116
+ {
7117
+ body: {
7118
+ ...requestConfig,
7119
+ template: normalizedRequestTemplate,
7120
+ // Cast systemd since we've processed SystemdServiceInput[] to RawSystemdService[]
7121
+ systemd: config.systemd,
7122
+ // Normalize git options - default config to {}
7123
+ git: normalizeGitOptions(config.git)
7124
+ }
7125
+ },
7126
+ logger
7127
+ ).catch((e) => {
7027
7128
  enhanceError(e);
7028
7129
  throw e;
7029
7130
  });
7030
- if (slowPathTimeout) clearTimeout(slowPathTimeout);
7031
7131
  const vmId = response.id;
7032
7132
  const vm = new Vm({ vmId, freestyle: this.freestyle });
7033
7133
  for (const key in builders) {
@@ -7180,11 +7280,12 @@ class VmSnapshotsNamespace {
7180
7280
  this.apiClient = apiClient;
7181
7281
  }
7182
7282
  async ensure(options) {
7283
+ const { logger, ...restOptions } = options;
7183
7284
  let requestOptions = {
7184
- ...options,
7185
- spec: isVmSpecLike$1(options.spec) ? cloneVmSpecTree(options.spec) : void 0,
7186
- snapshot: isVmSpecLike$1(options.snapshot) ? cloneVmSpecTree(options.snapshot) : void 0,
7187
- template: isVmTemplateLike$1(options.template) ? cloneVmTemplateTree(options.template) : options.template
7285
+ ...restOptions,
7286
+ spec: isVmSpecLike$1(restOptions.spec) ? cloneVmSpecTree(restOptions.spec) : void 0,
7287
+ snapshot: isVmSpecLike$1(restOptions.snapshot) ? cloneVmSpecTree(restOptions.snapshot) : void 0,
7288
+ template: isVmTemplateLike$1(restOptions.template) ? cloneVmTemplateTree(restOptions.template) : restOptions.template
7188
7289
  };
7189
7290
  if (isVmSpecLike$1(requestOptions.snapshot)) {
7190
7291
  if (isVmSpecLike$1(requestOptions.spec)) {
@@ -7222,7 +7323,8 @@ class VmSnapshotsNamespace {
7222
7323
  if (isVmTemplateLike$1(processedTemplate.raw.template)) {
7223
7324
  requestOptions.template = await ensureNestedTemplates(
7224
7325
  processedTemplate,
7225
- this
7326
+ this,
7327
+ logger
7226
7328
  );
7227
7329
  }
7228
7330
  }
@@ -7231,27 +7333,43 @@ class VmSnapshotsNamespace {
7231
7333
  "snapshots.ensure requires a template or spec to build a snapshot"
7232
7334
  );
7233
7335
  }
7234
- const startTime = Date.now();
7235
- return this.apiClient.post("/v1/vms/snapshots", {
7236
- body: {
7237
- ...requestOptions,
7238
- template: normalizeTemplateForRequest(
7239
- isVmTemplateLike$1(requestOptions.template) ? requestOptions.template.raw : requestOptions.template
7240
- )
7241
- }
7242
- }).then((response) => {
7243
- const elapsedTime = Date.now() - startTime;
7244
- if (elapsedTime > 5e3) {
7245
- console.log(
7246
- "Snapshot creation took longer than expected (> 5 seconds). This is slower because caches weren't available, but subsequent operations will be much faster."
7247
- );
7248
- }
7249
- return response;
7250
- }).catch((e) => {
7336
+ return postWithBackgroundLogger(
7337
+ this.apiClient,
7338
+ "/v1/vms/snapshots",
7339
+ {
7340
+ body: {
7341
+ ...requestOptions,
7342
+ template: normalizeTemplateForRequest(
7343
+ isVmTemplateLike$1(requestOptions.template) ? requestOptions.template.raw : requestOptions.template
7344
+ )
7345
+ }
7346
+ },
7347
+ logger
7348
+ ).catch((e) => {
7251
7349
  enhanceError(e);
7252
7350
  throw e;
7253
7351
  });
7254
7352
  }
7353
+ async create(options) {
7354
+ return this.ensure(options);
7355
+ }
7356
+ async delete({ snapshotId }) {
7357
+ const path = "/v1/vms/snapshots/{snapshot_id}";
7358
+ const response = await this.apiClient.fetch(
7359
+ this.apiClient.resolveUrl(path, { snapshot_id: snapshotId }),
7360
+ { method: "DELETE" }
7361
+ );
7362
+ if (!response.ok) {
7363
+ const errorBody = await response.json().catch(() => null);
7364
+ if (errorBody?.code) {
7365
+ throw errorFromJSON(errorBody);
7366
+ }
7367
+ throw new Error(
7368
+ `Failed to delete snapshot: ${response.status} ${response.statusText}`
7369
+ );
7370
+ }
7371
+ return await response.json();
7372
+ }
7255
7373
  }
7256
7374
  async function processTemplateTree(template) {
7257
7375
  if (template.raw.baseImage) {
@@ -7323,24 +7441,24 @@ async function processTemplateTree(template) {
7323
7441
  }
7324
7442
  return template;
7325
7443
  }
7326
- async function ensureNestedTemplates(template, snapshots) {
7444
+ async function ensureNestedTemplates(template, snapshots, logger) {
7327
7445
  const templates = [template];
7328
7446
  while (isVmTemplateLike$1(templates.at(-1)?.raw.template)) {
7329
7447
  const innerTemplate = templates.at(-1).raw.template;
7330
7448
  templates.at(-1).raw.template = void 0;
7331
7449
  templates.push(innerTemplate);
7332
7450
  }
7333
- return await layerTemplates(templates, snapshots);
7451
+ return await layerTemplates(templates, snapshots, logger);
7334
7452
  }
7335
- async function layerTemplates(templates, snapshots) {
7453
+ async function layerTemplates(templates, snapshots, logger) {
7336
7454
  if (templates.length === 1) {
7337
7455
  return templates[0];
7338
7456
  }
7339
- let lastSnapshotId = (await snapshots.ensure({ template: templates.pop() })).snapshotId;
7457
+ let lastSnapshotId = (await snapshots.ensure({ template: templates.pop(), logger })).snapshotId;
7340
7458
  while (templates.length > 1) {
7341
7459
  const template = templates.pop();
7342
7460
  template.raw.snapshotId = lastSnapshotId;
7343
- lastSnapshotId = (await snapshots.ensure({ template })).snapshotId;
7461
+ lastSnapshotId = (await snapshots.ensure({ template, logger })).snapshotId;
7344
7462
  }
7345
7463
  const outermost = templates.pop();
7346
7464
  outermost.raw.snapshotId = lastSnapshotId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freestyle-sandboxes",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "require": {