@adobe/aio-lib-sandbox 0.1.0-alpha.7 → 0.1.0-alpha.8

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/README.md CHANGED
@@ -74,12 +74,19 @@ const { Sandbox } = require('@adobe/aio-lib-sandbox')
74
74
  const sandbox = await Sandbox.create({
75
75
  name: 'my-sandbox',
76
76
  type: 'cpu:default',
77
+ idleTimeout: 900,
77
78
  maxLifetime: 3600,
78
79
  ports: [3000, 8080],
79
80
  envs: { API_KEY: 'your-api-key' }
80
81
  })
81
82
  ```
82
83
 
84
+ #### Sandbox lifetime model
85
+
86
+ A sandbox is always deleted when `maxLifetime` has elapsed. It will also be deleted after the `idleTimeout` has elapsed, if there has been no activity.
87
+
88
+ To keep a sandbox alive, send at least one command or check the status every `idleTimeout` seconds.
89
+
83
90
  ### Get Status
84
91
 
85
92
  ```js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/aio-lib-sandbox",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.8",
4
4
  "description": "JavaScript SDK for Adobe Runtime Sandboxes",
5
5
  "main": "src/index.js",
6
6
  "license": "Apache-2.0",
package/src/Sandbox.js CHANGED
@@ -42,6 +42,7 @@ class Sandbox {
42
42
  this.status = options.status
43
43
  this.cluster = options.cluster
44
44
  this.region = options.region
45
+ this.idleTimeout = options.idleTimeout
45
46
  this.maxLifetime = options.maxLifetime
46
47
 
47
48
  this.namespace = options.namespace
@@ -70,7 +71,10 @@ class Sandbox {
70
71
  * @param {string} [options.name] sandbox display name
71
72
  * @param {string} [options.type] sandbox type (default: `'cpu:default'`)
72
73
  * @param {string|object} [options.size] sandbox size tier (name or spec object)
73
- * @param {number} [options.maxLifetime] maximum lifetime in seconds
74
+ * @param {number} [options.idleTimeout] seconds of inactivity before the sandbox is terminated
75
+ * (default: 900, max: 10800). The idle timer resets on every WebSocket message or status-check
76
+ * request.
77
+ * @param {number} [options.maxLifetime] maximum lifetime in seconds (default: 3600, max: 10800)
74
78
  * @param {number[]} [options.ports] TCP ports to expose via preview URLs (default: `[]`)
75
79
  * @param {object} [options.envs] environment variables to inject into the sandbox
76
80
  * @param {object} [options.policy] network policy (e.g. egress allowlist)
@@ -84,6 +88,7 @@ class Sandbox {
84
88
  name: options.name,
85
89
  size: normalizeSize(options.size),
86
90
  type: options.type || 'cpu:default',
91
+ idleTimeout: options.idleTimeout || 900,
87
92
  maxLifetime: options.maxLifetime || 3600
88
93
  }
89
94
 
@@ -105,6 +110,7 @@ class Sandbox {
105
110
  status: payload.status,
106
111
  cluster: payload.cluster,
107
112
  region: payload.region,
113
+ idleTimeout: payload.idleTimeout,
108
114
  maxLifetime: payload.maxLifetime,
109
115
  previewUrls: parsePreviewUrls(payload.previewUrls),
110
116
  managementEndpoint: payload.managementEndpoint || null,
@@ -124,17 +130,23 @@ class Sandbox {
124
130
  * Credentials are read from the environment automatically.
125
131
  * Any value passed explicitly in `options` overrides the environment.
126
132
  *
133
+ * Pass the management endpoint so the request is sent to the correct host;
134
+ * falls back to `options.apiHost` when omitted.
135
+ *
127
136
  * @param {string} sandboxId the sandbox ID to look up
128
137
  * @param {object} [options] credential overrides
129
138
  * @param {string} [options.apiHost] Runtime API host
130
139
  * @param {string} [options.namespace] Runtime namespace
131
140
  * @param {string} [options.auth] Runtime API key
141
+ * @param {string} [options.managementEndpoint] per-sandbox management endpoint returned by
142
+ * `Sandbox.create()`. Falls back to `apiHost` otherwise.
132
143
  * @returns {Promise<Sandbox>} sandbox instance with `status` populated (not WebSocket-connected)
133
144
  */
134
145
  static async get (sandboxId, options = {}) {
135
146
  console.warn('[aio-lib-sandbox] alpha — APIs may change without notice')
136
147
  const creds = resolveCredentials(options)
137
- const url = `${creds.apiHost}/api/v1/namespaces/${creds.namespace}/sandboxes/${sandboxId}`
148
+ const base = options.managementEndpoint || creds.apiHost
149
+ const url = `${base}/api/v1/namespaces/${creds.namespace}/sandboxes/${sandboxId}`
138
150
  const payload = await apiRequest('GET', url, creds.apiKey)
139
151
 
140
152
  return new Sandbox({
@@ -143,7 +155,9 @@ class Sandbox {
143
155
  status: payload.status,
144
156
  cluster: payload.cluster,
145
157
  region: payload.region,
158
+ idleTimeout: payload.idleTimeout,
146
159
  maxLifetime: payload.maxLifetime,
160
+ managementEndpoint: payload.managementEndpoint || options.managementEndpoint || null,
147
161
  previewUrls: parsePreviewUrls(payload.previewUrls),
148
162
  namespace: creds.namespace,
149
163
  apiHost: creds.apiHost,
@@ -342,6 +342,98 @@ describe('Sandbox', () => {
342
342
  await expect(Sandbox.create({ name: 'no-creds' })).rejects.toThrow(SandboxInitializationError)
343
343
  })
344
344
 
345
+ test('sends default idleTimeout 900 and maxLifetime 3600 when not specified', async () => {
346
+ const mockFetch = jest.fn().mockResolvedValue({
347
+ ok: true,
348
+ json: () => Promise.resolve({
349
+ sandboxId: 'sb-defaults',
350
+ wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-defaults/exec',
351
+ status: 'ready',
352
+ token: 'tok-defaults',
353
+ idleTimeout: 900,
354
+ maxLifetime: 3600
355
+ })
356
+ })
357
+ global.fetch = mockFetch
358
+
359
+ const createPromise = Sandbox.create({
360
+ name: 'defaults-sandbox',
361
+ apiHost: 'https://runtime.example.net',
362
+ namespace: 'ns',
363
+ auth: 'uuid:key'
364
+ })
365
+
366
+ await new Promise(resolve => setImmediate(resolve))
367
+ sockets[0].open()
368
+ sockets[0].message({ type: 'auth.ok', sandboxId: 'sb-defaults' })
369
+ await createPromise
370
+
371
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
372
+ expect(body.idleTimeout).toBe(900)
373
+ expect(body.maxLifetime).toBe(3600)
374
+ })
375
+
376
+ test('forwards explicit idleTimeout in the request body', async () => {
377
+ const mockFetch = jest.fn().mockResolvedValue({
378
+ ok: true,
379
+ json: () => Promise.resolve({
380
+ sandboxId: 'sb-idle',
381
+ wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-idle/exec',
382
+ status: 'ready',
383
+ token: 'tok-idle',
384
+ idleTimeout: 1800,
385
+ maxLifetime: 3600
386
+ })
387
+ })
388
+ global.fetch = mockFetch
389
+
390
+ const createPromise = Sandbox.create({
391
+ name: 'idle-sandbox',
392
+ apiHost: 'https://runtime.example.net',
393
+ namespace: 'ns',
394
+ auth: 'uuid:key',
395
+ idleTimeout: 1800
396
+ })
397
+
398
+ await new Promise(resolve => setImmediate(resolve))
399
+ sockets[0].open()
400
+ sockets[0].message({ type: 'auth.ok', sandboxId: 'sb-idle' })
401
+ await createPromise
402
+
403
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
404
+ expect(body.idleTimeout).toBe(1800)
405
+ })
406
+
407
+ test('stores idleTimeout from the create response on the instance', async () => {
408
+ const mockFetch = jest.fn().mockResolvedValue({
409
+ ok: true,
410
+ json: () => Promise.resolve({
411
+ sandboxId: 'sb-store',
412
+ wsEndpoint: 'wss://runtime.example.net/api/v1/namespaces/ns/sandboxes/sb-store/exec',
413
+ status: 'ready',
414
+ token: 'tok-store',
415
+ idleTimeout: 1800,
416
+ maxLifetime: 3600
417
+ })
418
+ })
419
+ global.fetch = mockFetch
420
+
421
+ const createPromise = Sandbox.create({
422
+ name: 'store-sandbox',
423
+ apiHost: 'https://runtime.example.net',
424
+ namespace: 'ns',
425
+ auth: 'uuid:key',
426
+ idleTimeout: 1800
427
+ })
428
+
429
+ await new Promise(resolve => setImmediate(resolve))
430
+ sockets[0].open()
431
+ sockets[0].message({ type: 'auth.ok', sandboxId: 'sb-store' })
432
+ const sandbox = await createPromise
433
+
434
+ expect(sandbox.idleTimeout).toBe(1800)
435
+ })
436
+
345
437
  test('falls back to buildWebSocketEndpoint when wsEndpoint absent', async () => {
346
438
  const mockFetch = jest.fn().mockResolvedValue({
347
439
  ok: true,
@@ -394,6 +486,26 @@ describe('Sandbox', () => {
394
486
  expect(sandbox.cluster).toBe('cluster-b')
395
487
  })
396
488
 
489
+ test('stores idleTimeout from the get response on the instance', async () => {
490
+ global.fetch = jest.fn().mockResolvedValue({
491
+ ok: true,
492
+ json: () => Promise.resolve({
493
+ sandboxId: 'sb-get-idle',
494
+ status: 'running',
495
+ idleTimeout: 1200,
496
+ maxLifetime: 3600
497
+ })
498
+ })
499
+
500
+ const sandbox = await Sandbox.get('sb-get-idle', {
501
+ apiHost: 'https://runtime.example.net',
502
+ namespace: 'ns',
503
+ auth: 'uuid:key'
504
+ })
505
+
506
+ expect(sandbox.idleTimeout).toBe(1200)
507
+ })
508
+
397
509
  test('throws SandboxNotFoundError on 404', async () => {
398
510
  global.fetch = jest.fn().mockResolvedValue({
399
511
  ok: false,