@parel/sandbox-vercel 0.2.9 → 0.3.0

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.
Files changed (2) hide show
  1. package/dist/index.js +281 -48
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -237,6 +237,8 @@ var parel_plugin_default = {
237
237
 
238
238
  // src/index.ts
239
239
  var STORE_KEY = "vercel_sandbox_name";
240
+ var PROCESS_VIEW_STORE_PREFIX2 = "sandbox_process:";
241
+ var PORT_VIEW_STORE_PREFIX2 = "sandbox_port:";
240
242
  function stringConfig(value) {
241
243
  return typeof value === "string" && value.length > 0 ? value : void 0;
242
244
  }
@@ -317,7 +319,26 @@ var index_default = definePlugin({
317
319
  const teamId = stringConfig(ctx.config.teamId);
318
320
  const projectId = stringConfig(ctx.config.projectId);
319
321
  const destroyOnSessionEnd = ctx.config.destroyOnSessionEnd !== false;
322
+ const externalName = stringConfig(ctx.config.name);
323
+ const istore = ctx.instanceStore;
324
+ const state = istore ? {
325
+ async get(key) {
326
+ return (await istore.get(key))?.value ?? null;
327
+ },
328
+ set(key, value) {
329
+ return istore.set(key, value);
330
+ },
331
+ delete(key) {
332
+ return istore.delete(key);
333
+ },
334
+ list(prefix) {
335
+ return istore.list(prefix);
336
+ }
337
+ } : ctx.store;
338
+ const managed = Boolean(istore) && !externalName;
320
339
  let sandbox = null;
340
+ let sandboxRecovery = null;
341
+ let sandboxEpoch = 0;
321
342
  function credentials() {
322
343
  if (!token || !teamId || !projectId) {
323
344
  ctx.log.warn("Vercel Sandbox token, teamId, and projectId are required");
@@ -325,50 +346,238 @@ var index_default = definePlugin({
325
346
  }
326
347
  return { token, teamId, projectId };
327
348
  }
328
- function requireSandbox() {
329
- if (!sandbox) throw new Error("Vercel sandbox not available");
330
- return sandbox;
349
+ async function createRawSandbox() {
350
+ const creds = credentials();
351
+ if (!creds) return null;
352
+ const params = { ...buildCreateParams(ctx.config), ...creds };
353
+ const created = await VercelSandboxClient.create(params);
354
+ ctx.log.info(`Vercel sandbox created: ${created.name}`);
355
+ return created;
331
356
  }
332
- async function ensureSandbox() {
357
+ async function reconnectSandbox(name) {
358
+ const creds = credentials();
359
+ if (!creds) return null;
360
+ try {
361
+ const s = await VercelSandboxClient.get({ name, ...creds });
362
+ ctx.log.info(`Vercel sandbox connected: ${name}`);
363
+ return s;
364
+ } catch (err) {
365
+ const message = err instanceof Error ? err.message : String(err);
366
+ ctx.log.warn(`Failed to connect Vercel sandbox ${name}: ${message}`);
367
+ return null;
368
+ }
369
+ }
370
+ async function killSandboxById(name) {
371
+ const creds = credentials();
372
+ if (!creds) return false;
373
+ try {
374
+ const s = await VercelSandboxClient.get({ name, ...creds });
375
+ await s.delete();
376
+ return true;
377
+ } catch (err) {
378
+ const message = err instanceof Error ? err.message : String(err);
379
+ ctx.log.warn(`Failed to kill Vercel sandbox ${name}: ${message}`);
380
+ return false;
381
+ }
382
+ }
383
+ async function moveLegacyRecords(promote) {
384
+ for (const prefix of [PROCESS_VIEW_STORE_PREFIX2, PORT_VIEW_STORE_PREFIX2]) {
385
+ for (const key of await ctx.store.list(prefix)) {
386
+ if (promote) {
387
+ const record = await ctx.store.get(key);
388
+ if (record !== null) await state.set(key, record);
389
+ }
390
+ await ctx.store.delete(key);
391
+ }
392
+ }
393
+ }
394
+ async function clearViewRecords() {
395
+ for (const prefix of [PROCESS_VIEW_STORE_PREFIX2, PORT_VIEW_STORE_PREFIX2]) {
396
+ for (const key of await state.list(prefix)) {
397
+ await state.delete(key);
398
+ }
399
+ }
400
+ }
401
+ async function migrateLegacyHandle() {
402
+ if (!istore) return;
403
+ const legacy = await ctx.store.get(STORE_KEY);
404
+ if (!legacy) return;
405
+ const authoritative = await istore.get(STORE_KEY);
406
+ if (!authoritative) {
407
+ const reconnected = await reconnectSandbox(legacy);
408
+ if (!reconnected) {
409
+ await ctx.store.delete(STORE_KEY);
410
+ await moveLegacyRecords(false);
411
+ return;
412
+ }
413
+ if (await istore.cas(STORE_KEY, null, legacy)) {
414
+ await ctx.store.delete(STORE_KEY);
415
+ await moveLegacyRecords(true);
416
+ ctx.log.info(`Vercel sandbox ${legacy} migrated to the instance store`);
417
+ return;
418
+ }
419
+ }
420
+ if ((await istore.get(STORE_KEY))?.value !== legacy) {
421
+ await killSandboxById(legacy);
422
+ ctx.log.info(`Vercel legacy sandbox ${legacy} reaped (instance already has a sandbox)`);
423
+ }
424
+ await ctx.store.delete(STORE_KEY);
425
+ await moveLegacyRecords(false);
426
+ }
427
+ async function acquireSharedSandbox() {
428
+ if (!istore) throw new Error("acquireSharedSandbox requires an instance store");
429
+ await migrateLegacyHandle();
430
+ for (let attempt = 0; attempt < 3; attempt++) {
431
+ const entry = await istore.get(STORE_KEY);
432
+ if (entry) {
433
+ const reconnected = await reconnectSandbox(entry.value);
434
+ if (reconnected) {
435
+ const current = await istore.get(STORE_KEY);
436
+ if (current?.value === entry.value) return reconnected;
437
+ continue;
438
+ }
439
+ const fresh2 = await createRawSandbox();
440
+ if (!fresh2) return null;
441
+ if (await istore.cas(STORE_KEY, entry.version, fresh2.name)) {
442
+ ctx.log.warn(
443
+ `Vercel filesystem reset: sandbox ${entry.value} was unreachable, swapped in ${fresh2.name} (files in the previous sandbox are lost)`
444
+ );
445
+ await killSandboxById(entry.value);
446
+ await clearViewRecords();
447
+ return fresh2;
448
+ }
449
+ await killSandboxById(fresh2.name);
450
+ continue;
451
+ }
452
+ const fresh = await createRawSandbox();
453
+ if (!fresh) return null;
454
+ if (await istore.cas(STORE_KEY, null, fresh.name)) return fresh;
455
+ await killSandboxById(fresh.name);
456
+ }
457
+ throw new Error("Vercel sandbox acquisition kept losing instance-store races; giving up");
458
+ }
459
+ async function acquirePerSession() {
333
460
  const creds = credentials();
334
461
  if (!creds) return null;
335
462
  const savedName = await ctx.store.get(STORE_KEY);
336
463
  const name = stringConfig(ctx.config.name) ?? savedName;
337
464
  const params = { ...buildCreateParams(ctx.config), ...creds };
465
+ let next;
338
466
  if (name) {
339
- sandbox = await VercelSandboxClient.getOrCreate({
467
+ next = await VercelSandboxClient.getOrCreate({
340
468
  ...params,
341
469
  name
342
470
  });
343
471
  } else {
344
- sandbox = await VercelSandboxClient.create(params);
472
+ next = await VercelSandboxClient.create(params);
345
473
  }
346
- await ctx.store.set(STORE_KEY, sandbox.name);
347
- ctx.log.info(`Vercel sandbox ready: ${sandbox.name}`);
348
- return sandbox;
474
+ await ctx.store.set(STORE_KEY, next.name);
475
+ ctx.log.info(`Vercel sandbox ready: ${next.name}`);
476
+ return next;
349
477
  }
350
- async function disposeSandbox() {
351
- if (!sandbox) return;
352
- try {
353
- if (destroyOnSessionEnd) {
354
- await sandbox.delete();
355
- await ctx.store.delete(STORE_KEY);
356
- } else {
357
- await sandbox.stop();
358
- }
359
- } finally {
478
+ function acquire() {
479
+ return managed ? acquireSharedSandbox() : acquirePerSession();
480
+ }
481
+ async function ensureSandbox() {
482
+ const cached = sandbox;
483
+ if (cached) {
484
+ if (!managed) return cached;
485
+ const authoritative = await istore?.get(STORE_KEY);
486
+ if (authoritative?.value === cached.name) return cached;
487
+ if (sandbox === cached) sandbox = null;
488
+ }
489
+ if (!sandboxRecovery) {
490
+ const epoch = sandboxEpoch;
491
+ sandboxRecovery = acquire().then(async (s) => {
492
+ if (epoch !== sandboxEpoch) {
493
+ if (managed) {
494
+ throw new Error("Vercel sandbox recovery discarded: session torn down");
495
+ }
496
+ if (s && destroyOnSessionEnd) {
497
+ await killSandboxById(s.name);
498
+ await ctx.store.delete(STORE_KEY);
499
+ }
500
+ throw new Error("Vercel sandbox was torn down during recovery");
501
+ }
502
+ sandbox = s;
503
+ return s;
504
+ }).finally(() => {
505
+ sandboxRecovery = null;
506
+ });
507
+ }
508
+ return sandboxRecovery;
509
+ }
510
+ async function requireSandbox() {
511
+ const s = await ensureSandbox();
512
+ if (!s) throw new Error("Vercel sandbox not available");
513
+ return s;
514
+ }
515
+ async function releaseSandbox() {
516
+ sandboxEpoch++;
517
+ if (sandboxRecovery) {
518
+ await sandboxRecovery.catch(() => {
519
+ });
520
+ }
521
+ sandbox = null;
522
+ }
523
+ async function destroySandbox() {
524
+ sandboxEpoch++;
525
+ if (sandboxRecovery) {
526
+ await sandboxRecovery.catch(() => {
527
+ });
528
+ }
529
+ if (istore) {
360
530
  sandbox = null;
531
+ const entry = await istore.get(STORE_KEY);
532
+ if (entry) {
533
+ const retired = await istore.casDelete?.(STORE_KEY, entry.version) ?? false;
534
+ if (!retired && istore.casDelete) {
535
+ ctx.log.warn("Vercel stop skipped: the instance sandbox changed mid-stop");
536
+ await ctx.store.delete(STORE_KEY);
537
+ return;
538
+ }
539
+ if (!istore.casDelete) await istore.delete(STORE_KEY);
540
+ if (await killSandboxById(entry.value)) {
541
+ ctx.log.info(`Vercel sandbox destroyed: ${entry.value}`);
542
+ }
543
+ await clearViewRecords();
544
+ }
545
+ await ctx.store.delete(STORE_KEY);
546
+ return;
547
+ }
548
+ const target = sandbox?.name ?? await ctx.store.get(STORE_KEY);
549
+ sandbox = null;
550
+ if (target && await killSandboxById(target)) {
551
+ ctx.log.info(`Vercel sandbox destroyed: ${target}`);
552
+ }
553
+ await clearViewRecords();
554
+ await ctx.store.delete(STORE_KEY);
555
+ }
556
+ async function disposeSandbox() {
557
+ sandboxEpoch++;
558
+ if (sandboxRecovery) {
559
+ await sandboxRecovery.catch(() => {
560
+ });
561
+ }
562
+ const current = sandbox;
563
+ sandbox = null;
564
+ if (!current) return;
565
+ if (destroyOnSessionEnd) {
566
+ await current.delete();
567
+ await ctx.store.delete(STORE_KEY);
568
+ } else {
569
+ await current.stop();
361
570
  }
362
571
  }
363
572
  async function execCommand(command, opts) {
364
- const finished = await requireSandbox().runCommand(
365
- runParams(command, opts)
366
- );
573
+ const s = await requireSandbox();
574
+ const finished = await s.runCommand(runParams(command, opts));
367
575
  return commandResult(finished, opts);
368
576
  }
369
577
  async function shellCommand(command, opts) {
370
578
  const shell = opts?.shell ?? "sh";
371
- const finished = await requireSandbox().runCommand({
579
+ const s = await requireSandbox();
580
+ const finished = await s.runCommand({
372
581
  cmd: shell,
373
582
  args: ["-lc", command],
374
583
  cwd: opts?.cwd,
@@ -415,30 +624,32 @@ var index_default = definePlugin({
415
624
  },
416
625
  fs: {
417
626
  async readFile(path, opts) {
418
- const content = await requireSandbox().fs.readFile(path, {
627
+ const s = await requireSandbox();
628
+ const content = await s.fs.readFile(path, {
419
629
  encoding: opts?.encoding === "base64" ? null : "utf8"
420
630
  });
421
631
  const text = Buffer.isBuffer(content) ? content.toString("base64") : content;
422
632
  return opts?.maxChars ? limitOutput(text, opts.maxChars) : text;
423
633
  },
424
634
  async writeFile(path, content, opts) {
635
+ const s = await requireSandbox();
425
636
  const data = opts?.encoding === "base64" ? Buffer.from(content, "base64") : content;
426
- if (opts?.append && requireSandbox().fs.appendFile) {
427
- await requireSandbox().fs.appendFile?.(path, data);
637
+ if (opts?.append && s.fs.appendFile) {
638
+ await s.fs.appendFile(path, data);
428
639
  } else {
429
- await requireSandbox().fs.writeFile(path, data);
640
+ await s.fs.writeFile(path, data);
430
641
  }
431
642
  },
432
643
  async listDir(path) {
433
- return (await requireSandbox().fs.readdir(path, { withFileTypes: true })).map(
434
- (entry) => ({
435
- name: entry.name,
436
- type: mapDirentType(entry)
437
- })
438
- );
644
+ const s = await requireSandbox();
645
+ return (await s.fs.readdir(path, { withFileTypes: true })).map((entry) => ({
646
+ name: entry.name,
647
+ type: mapDirentType(entry)
648
+ }));
439
649
  },
440
650
  async stat(path) {
441
- const stats = await requireSandbox().fs.stat(path);
651
+ const s = await requireSandbox();
652
+ const stats = await s.fs.stat(path);
442
653
  return {
443
654
  path,
444
655
  type: mapStatsType(stats),
@@ -448,23 +659,28 @@ var index_default = definePlugin({
448
659
  };
449
660
  },
450
661
  async exists(path) {
451
- return requireSandbox().fs.exists(path);
662
+ const s = await requireSandbox();
663
+ return s.fs.exists(path);
452
664
  },
453
665
  async mkdir(path, opts) {
454
- await requireSandbox().fs.mkdir(path, { recursive: opts?.recursive });
666
+ const s = await requireSandbox();
667
+ await s.fs.mkdir(path, { recursive: opts?.recursive });
455
668
  },
456
669
  async remove(path, opts) {
457
- await requireSandbox().fs.rm(path, { recursive: opts?.recursive, force: true });
670
+ const s = await requireSandbox();
671
+ await s.fs.rm(path, { recursive: opts?.recursive, force: true });
458
672
  },
459
673
  async rename(from, to) {
460
- await requireSandbox().fs.rename(from, to);
674
+ const s = await requireSandbox();
675
+ await s.fs.rename(from, to);
461
676
  }
462
677
  },
463
678
  process: {
464
679
  exec: execCommand,
465
680
  shell: shellCommand,
466
681
  async spawn(command, opts) {
467
- const handle = await requireSandbox().runCommand({
682
+ const s = await requireSandbox();
683
+ const handle = await s.runCommand({
468
684
  ...runParams(command, opts),
469
685
  detached: true
470
686
  });
@@ -473,35 +689,52 @@ var index_default = definePlugin({
473
689
  },
474
690
  ports: {
475
691
  async expose(port) {
476
- return { port, url: requireSandbox().domain(port), protocol: "https" };
692
+ const s = await requireSandbox();
693
+ return { port, url: s.domain(port), protocol: "https" };
477
694
  }
478
695
  },
479
696
  lifecycle: {
480
697
  async isRunning() {
481
- return String(sandbox?.status ?? "running") === "running";
698
+ if (!sandbox) return false;
699
+ return String(sandbox.status ?? "running") === "running";
482
700
  },
483
701
  async stop() {
484
- await requireSandbox().stop();
702
+ if (istore) {
703
+ await destroySandbox();
704
+ return;
705
+ }
706
+ if (!sandbox) throw new Error("Vercel sandbox not available");
707
+ await sandbox.stop();
485
708
  },
486
709
  async extendTimeout(timeoutMs) {
487
- await requireSandbox().extendTimeout?.(timeoutMs);
710
+ const s = await requireSandbox();
711
+ await s.extendTimeout?.(timeoutMs);
488
712
  }
489
713
  }
490
714
  };
491
715
  ctx.hook(LifecycleEvent.SessionStart, async () => {
492
- await ensureSandbox();
716
+ sandbox = await acquire();
493
717
  });
494
718
  ctx.hook(LifecycleEvent.SessionResume, async () => {
495
- await ensureSandbox();
719
+ sandbox = null;
720
+ sandbox = await acquire();
496
721
  });
497
722
  ctx.hook(LifecycleEvent.SessionSuspend, async () => {
498
- if (sandbox) await ctx.store.set(STORE_KEY, sandbox.name);
723
+ if (!istore && sandbox) await ctx.store.set(STORE_KEY, sandbox.name);
499
724
  });
500
725
  ctx.hook(LifecycleEvent.SessionEnd, async () => {
501
- await disposeSandbox();
726
+ if (!istore) {
727
+ await disposeSandbox();
728
+ return;
729
+ }
730
+ if (ctx.instance?.ephemeral) {
731
+ await destroySandbox();
732
+ return;
733
+ }
734
+ await releaseSandbox();
502
735
  });
503
736
  ctx.provide(PAREL_SANDBOX_CAPABILITY, capability);
504
- const views = createSandboxCapabilityViews(capability, ctx.store);
737
+ const views = createSandboxCapabilityViews(capability, state);
505
738
  ctx.provide("filesystem", views.filesystem);
506
739
  ctx.provide("exec", views.exec);
507
740
  ctx.provide("process", views.process);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parel/sandbox-vercel",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "PAREL sandbox capability provider plugin for Vercel Sandbox.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "dependencies": {
29
29
  "@vercel/sandbox": "^2.1.1",
30
30
  "@parel/capability-sandbox": "0.2.0",
31
- "@parel/plugin-sdk": "0.10.1"
31
+ "@parel/plugin-sdk": "0.10.2"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^25.9.1",