@nimbus-sh/worker 0.5.0 → 0.7.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 (164) hide show
  1. package/README.md +2 -2
  2. package/dist/_shared/preview-host.d.ts +26 -8
  3. package/dist/_shared/preview-host.d.ts.map +1 -1
  4. package/dist/_shared/preview-host.js +51 -11
  5. package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
  6. package/dist/cirrus-npm-cjs.generated.js +1 -1
  7. package/dist/facets/cirrus-real.d.ts.map +1 -1
  8. package/dist/facets/cirrus-real.js +6 -5
  9. package/dist/facets/compose.d.ts +148 -0
  10. package/dist/facets/compose.d.ts.map +1 -0
  11. package/dist/facets/compose.js +146 -0
  12. package/dist/facets/durable-images.d.ts +18 -5
  13. package/dist/facets/durable-images.d.ts.map +1 -1
  14. package/dist/facets/durable-images.js +36 -7
  15. package/dist/facets/durable-slots.d.ts +1 -1
  16. package/dist/facets/durable-slots.d.ts.map +1 -1
  17. package/dist/facets/durable-slots.js +2 -1
  18. package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
  19. package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
  20. package/dist/facets/esbuild-bundle-pool.js +122 -0
  21. package/dist/facets/exec-telemetry.d.ts +8 -0
  22. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  23. package/dist/facets/manager.d.ts +337 -123
  24. package/dist/facets/manager.d.ts.map +1 -1
  25. package/dist/facets/manager.js +1100 -449
  26. package/dist/facets/process.d.ts +20 -16
  27. package/dist/facets/process.d.ts.map +1 -1
  28. package/dist/facets/process.js +59 -28
  29. package/dist/facets/real-vite-fs-shim.js +2 -2
  30. package/dist/facets/resident-identity.d.ts +23 -0
  31. package/dist/facets/resident-identity.d.ts.map +1 -0
  32. package/dist/facets/resident-identity.js +31 -0
  33. package/dist/facets/vite-dev-server.d.ts +25 -47
  34. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  35. package/dist/facets/vite-dev-server.js +185 -219
  36. package/dist/facets/wasm-image-digest.d.ts +25 -0
  37. package/dist/facets/wasm-image-digest.d.ts.map +1 -0
  38. package/dist/facets/wasm-image-digest.js +25 -0
  39. package/dist/facets/wasm-swap-registry.d.ts +51 -92
  40. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  41. package/dist/facets/wasm-swap-registry.js +68 -227
  42. package/dist/git/commands.d.ts +0 -1
  43. package/dist/git/commands.d.ts.map +1 -1
  44. package/dist/git/commands.js +0 -3
  45. package/dist/git/network-facet.d.ts.map +1 -1
  46. package/dist/git/network-facet.js +6 -5
  47. package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
  48. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  49. package/dist/loaders/child-process/spawn-pool.js +3 -2
  50. package/dist/loaders/generated-workers.d.ts +2 -2
  51. package/dist/loaders/generated-workers.d.ts.map +1 -1
  52. package/dist/loaders/generated-workers.js +3 -3
  53. package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
  54. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  55. package/dist/loaders/npm-resolve-preamble.js +34 -108
  56. package/dist/node-shims-artifact.generated.js +3 -3
  57. package/dist/npm/install-batch-facet.d.ts +3 -2
  58. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  59. package/dist/npm/install-batch-facet.js +45 -15
  60. package/dist/npm/installer.d.ts +22 -17
  61. package/dist/npm/installer.d.ts.map +1 -1
  62. package/dist/npm/installer.js +209 -193
  63. package/dist/npm/pre-bundle-facet.d.ts +1 -1
  64. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  65. package/dist/npm/pre-bundle-facet.js +1 -1
  66. package/dist/npm/resolve-facet.d.ts +6 -0
  67. package/dist/npm/resolve-facet.d.ts.map +1 -1
  68. package/dist/npm/resolve-one-facet.d.ts +21 -20
  69. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  70. package/dist/npm/resolve-one-facet.js +85 -84
  71. package/dist/npm/semver.d.ts +59 -0
  72. package/dist/npm/semver.d.ts.map +1 -0
  73. package/dist/npm/semver.js +226 -0
  74. package/dist/router/index.d.ts.map +1 -1
  75. package/dist/router/index.js +47 -10
  76. package/dist/router/public-directory.d.ts +5 -1
  77. package/dist/router/public-directory.d.ts.map +1 -1
  78. package/dist/router/public-directory.js +9 -2
  79. package/dist/router/remote-api.d.ts +18 -0
  80. package/dist/router/remote-api.d.ts.map +1 -1
  81. package/dist/router/remote-api.js +64 -5
  82. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  83. package/dist/runtime/cpython-resident.js +3 -1
  84. package/dist/runtime/html-entrypoint.d.ts +7 -0
  85. package/dist/runtime/html-entrypoint.d.ts.map +1 -1
  86. package/dist/runtime/html-entrypoint.js +18 -2
  87. package/dist/runtime/node-runner.d.ts +3 -0
  88. package/dist/runtime/node-runner.d.ts.map +1 -1
  89. package/dist/runtime/node-runner.js +7 -0
  90. package/dist/runtime/node-shims.d.ts.map +1 -1
  91. package/dist/runtime/node-shims.js +305 -29
  92. package/dist/runtime/opencode-facet-runner.js +2 -2
  93. package/dist/runtime/package-manager.d.ts +44 -1
  94. package/dist/runtime/package-manager.d.ts.map +1 -1
  95. package/dist/runtime/package-manager.js +168 -3
  96. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  97. package/dist/runtime/ruby-resident.js +2 -0
  98. package/dist/session/agent.d.ts.map +1 -1
  99. package/dist/session/agent.js +28 -1
  100. package/dist/session/helpers.d.ts +38 -0
  101. package/dist/session/helpers.d.ts.map +1 -1
  102. package/dist/session/helpers.js +76 -0
  103. package/dist/session/init-phases.d.ts +1 -1
  104. package/dist/session/init-phases.d.ts.map +1 -1
  105. package/dist/session/init-phases.js +2 -12
  106. package/dist/session/init.d.ts +15 -1
  107. package/dist/session/init.d.ts.map +1 -1
  108. package/dist/session/init.js +91 -603
  109. package/dist/session/keys.d.ts +4 -0
  110. package/dist/session/keys.d.ts.map +1 -1
  111. package/dist/session/keys.js +4 -0
  112. package/dist/session/nimbus-session.d.ts +90 -23
  113. package/dist/session/nimbus-session.d.ts.map +1 -1
  114. package/dist/session/nimbus-session.js +200 -110
  115. package/dist/session/npm-install-port.d.ts +16 -0
  116. package/dist/session/npm-install-port.d.ts.map +1 -0
  117. package/dist/session/npm-install-port.js +37 -0
  118. package/dist/session/port-capability.d.ts +90 -0
  119. package/dist/session/port-capability.d.ts.map +1 -1
  120. package/dist/session/port-capability.js +219 -9
  121. package/dist/session/programmatic.d.ts +125 -5
  122. package/dist/session/programmatic.d.ts.map +1 -1
  123. package/dist/session/programmatic.js +439 -89
  124. package/dist/session/routes.d.ts +39 -17
  125. package/dist/session/routes.d.ts.map +1 -1
  126. package/dist/session/routes.js +67 -131
  127. package/dist/session/rpc.d.ts +39 -16
  128. package/dist/session/rpc.d.ts.map +1 -1
  129. package/dist/session/rpc.js +137 -130
  130. package/dist/session/serving-port.d.ts +27 -0
  131. package/dist/session/serving-port.d.ts.map +1 -0
  132. package/dist/session/serving-port.js +27 -0
  133. package/dist/session/start-real-vite.d.ts +12 -0
  134. package/dist/session/start-real-vite.d.ts.map +1 -1
  135. package/dist/session/start-real-vite.js +4 -4
  136. package/dist/session/supervisor-op.d.ts +41 -4
  137. package/dist/session/supervisor-op.d.ts.map +1 -1
  138. package/dist/session/supervisor-op.js +101 -73
  139. package/dist/session/supervisor-rpc.d.ts +7 -7
  140. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  141. package/dist/session/supervisor-rpc.js +6 -3
  142. package/dist/session/vite-command.d.ts +18 -0
  143. package/dist/session/vite-command.d.ts.map +1 -0
  144. package/dist/session/vite-command.js +609 -0
  145. package/dist/session/ws.d.ts +61 -0
  146. package/dist/session/ws.d.ts.map +1 -1
  147. package/dist/session/ws.js +122 -12
  148. package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
  149. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  150. package/dist/shell/npm-bin-entrypoints.js +12 -85
  151. package/package.json +24 -4
  152. package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
  153. package/public/index.html +1 -1
  154. package/scripts/bundle-facet-workers.mjs +1 -1
  155. package/scripts/bundle-npm-cjs.mjs +2 -2
  156. package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
  157. package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
  158. package/dist/facets/on-demand-bundle-gate.js +0 -125
  159. package/dist/npm/install-args.d.ts +0 -21
  160. package/dist/npm/install-args.d.ts.map +0 -1
  161. package/dist/npm/install-args.js +0 -70
  162. package/dist/npm/npm-log.d.ts +0 -31
  163. package/dist/npm/npm-log.d.ts.map +0 -1
  164. package/dist/npm/npm-log.js +0 -51
@@ -8,17 +8,15 @@
8
8
  import { staticStdinReader } from '@nimbus-sh/core/shell/stdin-adapter.js';
9
9
  import { DurableObject as CloudflareDurableObject } from 'cloudflare:workers';
10
10
  import { SqliteVFS } from '@nimbus-sh/core/vfs/sqlite-vfs.js';
11
- import { FacetManager } from '../facets/manager.js';
12
- import { resolveDurableWorkerImage } from '../facets/durable-images.js';
13
- import { FacetProcessManager } from '../facets/process.js';
11
+ import { composeFacetManager } from '../facets/compose.js';
12
+ import { FacetProcessManager, textBytes } from '../facets/process.js';
14
13
  import { ChildProcessSpawnPool } from '../loaders/child-process/spawn-pool.js';
15
14
  import { SessionProcessSupervisor } from '@nimbus-sh/core/runtime/session-process-supervisor.js';
16
15
  import { PID_GEN_STRIDE } from '@nimbus-sh/core/runtime/process-table.js';
17
16
  import { CRED_KERNEL, CRED_SESSION_USER, } from '@nimbus-sh/core/runtime/os-contracts.js';
18
17
  import { PortRegistry } from '@nimbus-sh/core/runtime/port-registry.js';
18
+ import { EsbuildBundlePool } from '../facets/esbuild-bundle-pool.js';
19
19
  import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
20
- import { ESBUILD_TRANSFORM_WORKER_ID, esbuildTransformWorkerCode, } from '../facets/esbuild-transform.js';
21
- import { fetchEsbuildWasmBytes } from '../runtime/esbuild-wasm-bytes.js';
22
20
  import { registerAllocObserver } from '@nimbus-sh/platform/heavy-alloc-coord.js';
23
21
  // S10: oom-discriminator helpers (recordFailure, getFailures,
24
22
  // getLastRpcFrame, getLastFacetId, snapshotForStorage, rehydrateFromStorage)
@@ -35,7 +33,7 @@ import { registerAllocObserver } from '@nimbus-sh/platform/heavy-alloc-coord.js'
35
33
  import { adoptCtxExports } from '@nimbus-sh/fabric/composition.js';
36
34
  import { NIMBUS_VERSION, DEFAULT_MOUNT_POINTS, CF_COMPAT_DATE } from '@nimbus-sh/core/constants.js';
37
35
  import { seedBaseFilesystem } from '@nimbus-sh/core/workspace';
38
- import { seedProject } from '@nimbus-sh/core/vfs/seed-project.js';
36
+ import { seedProject, SEED_PROJECT_NAME } from '@nimbus-sh/core/vfs/seed-project.js';
39
37
  import { BASE_PATH_HEADER } from '../_shared/session-router.js';
40
38
  import { ATTACH_BOOTSTRAP_JTI_KEY_PREFIX, SESSION_DESTROYED_KEY } from './keys.js';
41
39
  import { appendScrollback } from './state-store.js';
@@ -56,13 +54,13 @@ import { initSession as _w11InitSession } from './init.js';
56
54
  import { wsMessage as _wsDoMessage, wsClose as _wsDoClose, wsError as _wsDoError, safePersistRing as _wsDoSafePersistRing, } from './ws.js';
57
55
  // S8: Supervisor RPC + W8 cp* + legacy VFS impls extracted.
58
56
  import * as _rpc from './rpc.js';
59
- import { sessionSupervisorOp } from './supervisor-op.js';
60
- import { processHostFor } from '../loaders/process-host.js';
57
+ import { buildSessionSupervisorOps } from './supervisor-op.js';
61
58
  // The supervisor terminates a facet's outbound sockets so inbound frames
62
59
  // arrive as supervisor replies (VFS coherence witness 3).
63
60
  import { WebSocketRelay } from './ws-relay.js';
64
61
  // S9: HTTP fetch routing extracted (combined S9a + S9b).
65
62
  import * as _routes from './routes.js';
63
+ import * as _portCapability from './port-capability.js';
66
64
  // Programmatic SDK RPC surface.
67
65
  import * as _programmatic from './programmatic.js';
68
66
  // S10: heap probe + W5 OOM-ring persistence extracted.
@@ -170,7 +168,7 @@ Cloud-native development environment on Cloudflare Workers.
170
168
  | --- | --- |
171
169
  | Run JavaScript | \`node hello.js\` |
172
170
  | Install npm packages | \`npm install <pkg>\` |
173
- | Start the starter app | \`cd app && npm install && npm run dev\` |
171
+ | Start the starter app | \`cd ${SEED_PROJECT_NAME} && npm install && npm run dev\` |
174
172
  | Run a Cloudflare Worker locally | \`nimbus-wrangler dev\` |
175
173
 
176
174
  ## AI Models
@@ -245,12 +243,15 @@ The editor opens this file in Markdown preview mode by default. Use
245
243
  export class NimbusSession extends CloudflareDurableObject {
246
244
  // this.ctx and this.env are provided by the DurableObject base class
247
245
  sqliteFs = null;
248
- runtimeFsBridges = null;
249
246
  kernel = null;
250
247
  shell = null;
251
248
  shellProcessPid = null;
252
249
  terminal = null;
253
- facetManager = null;
250
+ /** The composed manager is the one field; `.manager` is derived. */
251
+ facetManagerComposed = null;
252
+ get facetManager() {
253
+ return this.facetManagerComposed?.manager ?? null;
254
+ }
254
255
  /** W8: child_process broker. Lazy — only constructed when first cp* RPC arrives. */
255
256
  facetProcessManager = null;
256
257
  /**
@@ -260,6 +261,12 @@ export class NimbusSession extends CloudflareDurableObject {
260
261
  */
261
262
  webSocketRelay = null;
262
263
  esbuildService = null;
264
+ /**
265
+ * The session's single esbuild facet pool, shared by the npm installer's
266
+ * pre-bundler and the dev server's on-demand /@modules/ path. Lazy; see
267
+ * ensureBundlePool. Disposed with the installer and dev server.
268
+ */
269
+ bundlePool = null;
263
270
  viteDevServer = null;
264
271
  /**
265
272
  * runtime primitive support (P5): PID + port the default-Cirrus vite shim is
@@ -393,6 +400,13 @@ export class NimbusSession extends CloudflareDurableObject {
393
400
  sessionBasePath = '';
394
401
  /** Have we attempted to hydrate sessionBasePath from storage yet? */
395
402
  sessionBasePathHydrated = false;
403
+ /**
404
+ * The origin this session was last reached at (`https://host`), remembered
405
+ * beside the base path so the session can spell a path-form URL for one
406
+ * of its own applications — `nimbus expose` printing one, `apps.list`
407
+ * carrying one — on a deployment with no preview-host suffix.
408
+ */
409
+ sessionOrigin = '';
396
410
  /**
397
411
  * Has the "wrangler is aliased to nimbus-wrangler" banner been shown
398
412
  * this session? Reset on WebSocket close/reopen so a reconnecting user
@@ -545,8 +559,8 @@ export class NimbusSession extends CloudflareDurableObject {
545
559
  * it arrives as a new invocation, so the chunk runs against a fresh CPU
546
560
  * budget rather than the one the launch has already been spending.
547
561
  */
548
- _scheduleLaunchTurn() {
549
- return timers(this, this.ctx).schedule('resident-launch', Date.now());
562
+ _scheduleLaunchTurn(notBefore = 0) {
563
+ return timers(this, this.ctx).schedule('resident-launch', Math.max(Date.now(), notBefore));
550
564
  }
551
565
  /**
552
566
  * Convenience: the full URL prefix for the Vite dev server inside this
@@ -572,6 +586,9 @@ export class NimbusSession extends CloudflareDurableObject {
572
586
  const saved = await this.ctx.storage.get('session-base-path');
573
587
  if (typeof saved === 'string')
574
588
  this.sessionBasePath = saved;
589
+ const origin = await this.ctx.storage.get('session-origin');
590
+ if (typeof origin === 'string')
591
+ this.sessionOrigin = origin;
575
592
  }
576
593
  catch { /* storage unavailable — stay empty */ }
577
594
  this.sessionBasePathHydrated = true;
@@ -584,6 +601,23 @@ export class NimbusSession extends CloudflareDurableObject {
584
601
  }
585
602
  catch { }
586
603
  }
604
+ // The router forwards the request under its own origin; a preview host
605
+ // is not this session's control-plane origin, so only a request that
606
+ // carried the base path (a /s/<sid>/… door) teaches it.
607
+ if (fromHeader) {
608
+ let origin = '';
609
+ try {
610
+ origin = new URL(request.url).origin;
611
+ }
612
+ catch { /* not an absolute URL — nothing to learn */ }
613
+ if (origin && origin !== this.sessionOrigin) {
614
+ this.sessionOrigin = origin;
615
+ try {
616
+ await this.ctx.storage.put('session-origin', origin);
617
+ }
618
+ catch { }
619
+ }
620
+ }
587
621
  }
588
622
  /**
589
623
  * Consume a single-use attach bootstrap token id (`jti`) — set-if-absent.
@@ -605,27 +639,48 @@ export class NimbusSession extends CloudflareDurableObject {
605
639
  // dispatches by name from the stub) keeps working. Method bodies are
606
640
  // 1-line delegators that pass `this as any` (per plan §IX rec 1 +
607
641
  // DEFECT-D1: ctx is protected and not on a public interface).
642
+ /**
643
+ * The one supervisor-op handler this session's bindings, loopback stubs and
644
+ * `_rpc*` delegates all dispatch through — native filesystem ops against
645
+ * the shared bridge store, session overrides for the accounting-carrying
646
+ * reads and the output stream, and the canonical route table for the rest.
647
+ * Lazy: sqliteFs exists only after ensureSqliteFs().
648
+ */
649
+ _supervisorOps = null;
650
+ supervisorOps() {
651
+ if (!this._supervisorOps)
652
+ this._supervisorOps = buildSessionSupervisorOps(this);
653
+ return this._supervisorOps;
654
+ }
655
+ /** The pid-keyed filesystem bridge behind the supervisor ops. */
656
+ supervisorBridge(pid) {
657
+ return this.supervisorOps().bridge(pid);
658
+ }
659
+ /** Drop a dead pid's supervisor bridge — its credential stops being valid. */
660
+ supervisorForgetBridge(pid) {
661
+ this._supervisorOps?.forget(pid);
662
+ }
608
663
  // Supervisor RPC (file/log/HMR/batch)
609
664
  supervisorOp(envelope) {
610
- return sessionSupervisorOp(this, envelope);
665
+ return this.supervisorOps().dispatch(envelope);
611
666
  }
612
- async _rpcReadFile(path, pid) { return _rpc._rpcReadFile(this, path, pid); }
613
- async _rpcReadFileBytes(path, pid) { return _rpc._rpcReadFileBytes(this, path, pid); }
667
+ async _rpcReadFile(path, pid, cred) { return _rpc._rpcReadFile(this, path, pid, cred); }
668
+ async _rpcReadFileBytes(path, pid, cred) { return _rpc._rpcReadFileBytes(this, path, pid, cred); }
614
669
  async _rpcInnerDoFetch(req) { return _rpc._rpcInnerDoFetch(this, req); }
615
- async _rpcWriteFile(path, content, pid) { return _rpc._rpcWriteFile(this, path, content, pid); }
670
+ async _rpcWriteFile(path, content, pid, cred) { return _rpc._rpcWriteFile(this, path, content, pid, cred); }
616
671
  async _rpcWriteProtectedRootFile(rootPath, path, content) {
617
672
  return _rpc._rpcWriteProtectedRootFile(this, rootPath, path, content);
618
673
  }
619
- async _rpcStat(path, pid) { return _rpc._rpcStat(this, path, pid); }
620
- async _rpcLstat(path, pid) { return _rpc._rpcLstat(this, path, pid); }
674
+ async _rpcStat(path, pid, cred) { return _rpc._rpcStat(this, path, pid, cred); }
675
+ async _rpcLstat(path, pid, cred) { return _rpc._rpcLstat(this, path, pid, cred); }
621
676
  async _rpcHasLegacySymlinkUnder(path, pid) {
622
677
  return _rpc._rpcHasLegacySymlinkUnder(this, path, pid);
623
678
  }
624
679
  async _rpcUtimes(path, atimeMs, mtimeMs, pid) {
625
680
  return _rpc._rpcUtimes(this, path, atimeMs, mtimeMs, pid);
626
681
  }
627
- async _rpcChmod(path, mode, pid) {
628
- return _rpc._rpcChmod(this, path, mode, pid);
682
+ async _rpcChmod(path, mode, pid, cred) {
683
+ return _rpc._rpcChmod(this, path, mode, pid, cred);
629
684
  }
630
685
  async _rpcAccess(path, mode, pid) {
631
686
  return _rpc._rpcAccess(this, path, mode, pid);
@@ -636,11 +691,11 @@ export class NimbusSession extends CloudflareDurableObject {
636
691
  async _rpcSetUmask(mask, pid) {
637
692
  return _rpc._rpcSetUmask(this, mask, pid);
638
693
  }
639
- async _rpcReaddir(path, pid) { return _rpc._rpcReaddir(this, path, pid); }
640
- async _rpcExists(path, pid) { return _rpc._rpcExists(this, path, pid); }
641
- async _rpcMkdir(path, pid) { return _rpc._rpcMkdir(this, path, pid); }
694
+ async _rpcReaddir(path, pid, cred) { return _rpc._rpcReaddir(this, path, pid, cred); }
695
+ async _rpcExists(path, pid, cred) { return _rpc._rpcExists(this, path, pid, cred); }
696
+ async _rpcMkdir(path, pid, cred) { return _rpc._rpcMkdir(this, path, pid, cred); }
642
697
  async _rpcRmdir(path, pid) { return _rpc._rpcRmdir(this, path, pid); }
643
- async _rpcRename(from, to, pid) { return _rpc._rpcRename(this, from, to, pid); }
698
+ async _rpcRename(from, to, pid, cred) { return _rpc._rpcRename(this, from, to, pid, cred); }
644
699
  async _rpcReadlink(path, pid) { return _rpc._rpcReadlink(this, path, pid); }
645
700
  async _rpcSymlink(target, path, pid) { return _rpc._rpcSymlink(this, target, path, pid); }
646
701
  async _rpcFsRevision(path, pid) { return _rpc._rpcFsRevision(this, path, pid); }
@@ -670,8 +725,8 @@ export class NimbusSession extends CloudflareDurableObject {
670
725
  return _rpc._rpcFsWrite(this, handleId, offset, bytes, pid);
671
726
  }
672
727
  async _rpcFsClose(handleId, pid) { return _rpc._rpcFsClose(this, handleId, pid); }
673
- async _rpcFsReadRange(path, offset, length, pid) {
674
- return _rpc._rpcFsReadRange(this, path, offset, length, pid);
728
+ async _rpcFsReadRange(path, offset, length, pid, cred) {
729
+ return _rpc._rpcFsReadRange(this, path, offset, length, pid, cred);
675
730
  }
676
731
  async _rpcFsReadRangeUncached(path, offset, length, pid) {
677
732
  return _rpc._rpcFsReadRangeUncached(this, path, offset, length, pid);
@@ -774,6 +829,14 @@ export class NimbusSession extends CloudflareDurableObject {
774
829
  async _rpcExposePort(port, options) {
775
830
  return _programmatic.rpcExposePort(this, port, options);
776
831
  }
832
+ // The identity-centric application surface: one implementation each in
833
+ // programmatic.ts, addressed by port, pid, name or owner.
834
+ async _rpcExposeApp(target, options) {
835
+ return _programmatic.rpcExposeApp(this, target, options);
836
+ }
837
+ async _rpcListApps() { return _programmatic.rpcListApps(this); }
838
+ async _rpcRotateLink(target) { return _programmatic.rpcRotateLink(this, target); }
839
+ async _rpcRemoveApp(target) { return _programmatic.rpcRemoveApp(this, target); }
777
840
  async _rpcEnsureDurableApp(input) {
778
841
  return _programmatic.rpcEnsureDurableApp(this, input);
779
842
  }
@@ -783,7 +846,15 @@ export class NimbusSession extends CloudflareDurableObject {
783
846
  async _rpcUnexposePort(port) { return _programmatic.rpcUnexposePort(this, port); }
784
847
  /** Capability-authenticated port route, for an embedder holding the token. */
785
848
  async _rpcRouteCapabilityPort(port, capability, request, innerPath) {
786
- return _routes.routeCapabilityPort(this, port, capability, request, innerPath);
849
+ return _portCapability.routeCapabilityPort(this, port, capability, request, innerPath);
850
+ }
851
+ /** The port route's dev-server restore, delegated — lives in routes.ts where the other session routes do. */
852
+ restorePersistedDevServer(onlyPort) {
853
+ return _routes.restorePersistedDevServer(this, onlyPort);
854
+ }
855
+ /** The port route's in-DO HMR accept, delegated — same reason. */
856
+ acceptCirrusHmrWs(request) {
857
+ return _routes.acceptCirrusHmrWs(this, request);
787
858
  }
788
859
  /**
789
860
  * The port route's recovery seam: a durable application journalled but
@@ -791,10 +862,20 @@ export class NimbusSession extends CloudflareDurableObject {
791
862
  * manager decides 'started' | 'absent' | 'failed'; the route maps them.
792
863
  */
793
864
  async ensureDurableAppOnPort(port) {
865
+ // Stood up the same way the alarm pump stands itself up: a port request
866
+ // is the first thing to reach a replacement instance as often as an
867
+ // alarm is, and the launch it re-drives needs a filesystem to be
868
+ // re-driven onto — measured live (staging 2026-09-14): without it the
869
+ // re-drive failed with "a resident process needs a session filesystem".
870
+ this.ensureSqliteFs();
794
871
  this.ensureFacetManager();
795
872
  return this.facetManager.ensureDurableAppOnPort(port);
796
873
  }
797
- async _rpcDeleteFile(path, options) { return _programmatic.rpcDeleteFile(this, path, options); }
874
+ async _rpcDeleteFile(path, options, cred) { return _programmatic.rpcDeleteFile(this, path, options, cred); }
875
+ /** Colocated embedders only (DO stub); not on the remote dispatcher. See `rpcSpawnWorker`. */
876
+ async _rpcSpawnWorker(workerCode, command, cwd, opts) {
877
+ return _programmatic.rpcSpawnWorker(this, workerCode, command, cwd, opts);
878
+ }
798
879
  async _rpcDestroy(options) { return _programmatic.rpcDestroy(this, options); }
799
880
  // Legacy VFS (direct method calls)
800
881
  vfsReadFile(path) { return _rpc.vfsReadFile(this, path); }
@@ -883,6 +964,7 @@ export class NimbusSession extends CloudflareDurableObject {
883
964
  console.warn('[nimbus/W5] ring rehydrate failed:', e?.message);
884
965
  });
885
966
  }
967
+ return this.sqliteFs;
886
968
  }
887
969
  // ── W5 Lever 5: ring buffer persistence on DO storage ─────────────────
888
970
  // Storage key W5_RING_STORAGE_KEY lives in ./nimbus-session-keys.ts (S5).
@@ -906,72 +988,72 @@ export class NimbusSession extends CloudflareDurableObject {
906
988
  _w5PersistRing() {
907
989
  return _diag.persistRing(this, this.ctx);
908
990
  }
991
+ /** The session's esbuild facet pool provider; constructing it does no work. */
992
+ ensureBundlePool() {
993
+ if (!this.bundlePool)
994
+ this.bundlePool = new EsbuildBundlePool(this.env, this.ctx);
995
+ return this.bundlePool;
996
+ }
997
+ /**
998
+ * The session's FacetManager, composed through the one factory every host
999
+ * uses (`facets/compose.ts`). What is wired here is only what is the
1000
+ * session's: the terminal a spawn is announced on, the scrollback a notice
1001
+ * survives in, the alarm that grants a launch its next turn, and the exit
1002
+ * report that keeps the process table honest. The isolated esbuild
1003
+ * transform and the durable image-store fallback are the factory's.
1004
+ */
909
1005
  ensureFacetManager() {
910
- if (!this.facetManager) {
911
- this.facetManager = new FacetManager(this.ctx, this.env, this.processes, this.portRegistry, processHostFor, {
912
- onExternalExit: (pid, code, reason) => this._reportExternalExit(pid, code, reason),
913
- requestLaunchTurn: () => { void this._scheduleLaunchTurn(); },
914
- notify: (line) => this._notifySession(line),
915
- transformLargeEsm: async (code, options) => {
916
- const loader = Reflect.get(this.env, 'LOADER');
917
- if (!loader || typeof loader.get !== 'function') {
918
- throw new Error('Nimbus: env.LOADER unavailable for isolated esbuild transform');
919
- }
920
- const assets = Reflect.get(this.env, 'ASSETS');
921
- if (!assets || typeof assets.fetch !== 'function') {
922
- throw new Error('Nimbus: env.ASSETS unavailable for isolated esbuild transform');
923
- }
924
- const worker = await loader.get(ESBUILD_TRANSFORM_WORKER_ID, async () => esbuildTransformWorkerCode(await fetchEsbuildWasmBytes({ ASSETS: assets })));
925
- const transformClass = worker.getDurableObjectClass('EsbuildTransformFacet');
926
- const facet = this.ctx.facets.get(`esbuild-transform-${ESBUILD_TRANSFORM_WORKER_ID}`, async () => ({ class: transformClass }));
927
- return facet.transform(code, options);
928
- },
929
- onSpawn: (pid, command, longRunning) => {
930
- const attachedTty = this.processes.get(pid)?.attachedTty === true;
931
- if (longRunning) {
932
- try {
933
- this.processes.openInput(pid);
1006
+ if (!this.facetManagerComposed) {
1007
+ // The manager is composed over the filesystem, so the filesystem comes
1008
+ // first. Cheap and idempotent; every caller already stood it up or is
1009
+ // about to.
1010
+ this.ensureSqliteFs();
1011
+ this.facetManagerComposed = composeFacetManager({
1012
+ ctx: this.ctx,
1013
+ env: this.env,
1014
+ processes: this.processes,
1015
+ portRegistry: this.portRegistry,
1016
+ vfs: this.sqliteFs,
1017
+ ...(this.esbuildService ? { esbuild: this.esbuildService } : {}),
1018
+ hooks: {
1019
+ onExternalExit: (pid, code, reason) => this._reportExternalExit(pid, code, reason),
1020
+ requestLaunchTurn: (notBefore) => { void this._scheduleLaunchTurn(notBefore); },
1021
+ notify: (line) => this._notifySession(line),
1022
+ onSpawn: (pid, command, longRunning) => {
1023
+ const attachedTty = this.processes.get(pid)?.attachedTty === true;
1024
+ if (longRunning) {
1025
+ try {
1026
+ this.processes.openInput(pid);
1027
+ }
1028
+ catch { }
934
1029
  }
935
- catch { }
936
- }
937
- // Only surface long-running / user-visible spawns to keep
938
- // the terminal uncluttered. Short `node <file>` evals also
939
- // get a line because users want the pid for `logs`/`kill`.
940
- if (!this.terminal)
941
- return;
942
- const label = longRunning ? 'started (long-running)' : 'started';
943
- this.terminal.write(`\x1b[2m[facet ${label}: pid=${pid} cmd="${command}"]\x1b[0m\r\n`);
944
- // Structured event so the tabs UI can auto-open a log tab
945
- // for long-running processes (vite, wrangler dev, etc.).
946
- notifyTerminalEvent(this.terminal, {
947
- type: 'spawn', pid, command, longRunning, attachedTty,
948
- });
949
- },
950
- // The fallback resolver a self-owned durable spawn re-drives
951
- // through: read the image blobs the spawn persisted under
952
- // `.nimbus/images/<sha256>` and restore the launch's env and
953
- // modules from them. An embedder-owned launch answers its own
954
- // bookkeeping through resolveWorkerLaunch instead — this hook is
955
- // only the fallback for applications nobody else is keeping, and
956
- // because it cannot re-mint a live globalOutbound binding the
957
- // spawn path refuses such a launch under it.
958
- resolveWorkerLaunchFallback: async (recipe) => {
959
- this.ensureSqliteFs();
960
- return resolveDurableWorkerImage(this.sqliteFs, recipe);
1030
+ // Only surface long-running / user-visible spawns to keep
1031
+ // the terminal uncluttered. Short `node <file>` evals also
1032
+ // get a line because users want the pid for `logs`/`kill`.
1033
+ if (!this.terminal)
1034
+ return;
1035
+ const label = longRunning ? 'started (long-running)' : 'started';
1036
+ this.terminal.write(`\x1b[2m[facet ${label}: pid=${pid} cmd="${command}"]\x1b[0m\r\n`);
1037
+ // Structured event so the tabs UI can auto-open a log tab
1038
+ // for long-running processes (vite, wrangler dev, etc.).
1039
+ notifyTerminalEvent(this.terminal, {
1040
+ type: 'spawn', pid, command, longRunning, attachedTty,
1041
+ });
1042
+ },
961
1043
  },
962
1044
  });
963
1045
  }
964
- if (this.facetManager && this.sqliteFs) {
965
- this.facetManager.setVfs(this.sqliteFs);
966
- // W3.5 Fix B: share the session's lazy esbuildService with the
967
- // FacetManager so the bundle's ESM→CJS pre-pass doesn't pay
968
- // wasm-init twice. If the session hasn't constructed one yet,
969
- // FacetManager will lazy-create its own on first exec — same
970
- // wasm bytes, same ~10ms init cost, just paid once per surface.
971
- if (this.esbuildService) {
972
- this.facetManager.setEsbuildService(this.esbuildService);
973
- }
1046
+ const composed = this.facetManagerComposed;
1047
+ // W3.5 Fix B: share the session's lazy esbuildService with the
1048
+ // FacetManager so the bundle's ESM→CJS pre-pass doesn't pay
1049
+ // wasm-init twice. The session may construct it after the manager
1050
+ // exists, so the share is re-offered on every call; FacetManager
1051
+ // otherwise lazy-creates its own on first exec — same wasm bytes,
1052
+ // same ~10ms init cost, just paid once per surface.
1053
+ if (this.esbuildService) {
1054
+ composed.manager.setEsbuildService(this.esbuildService);
974
1055
  }
1056
+ return composed;
975
1057
  }
976
1058
  /**
977
1059
  * The supervisor-owned WebSocket relay. Lazy, because most sessions never
@@ -1013,29 +1095,29 @@ export class NimbusSession extends CloudflareDurableObject {
1013
1095
  try {
1014
1096
  const parsed = CpFacetDirectPayloadSchema.safeParse(JSON.parse(codeJson));
1015
1097
  if (!parsed.success) {
1016
- hooks.onStderr('child_process: facet dispatch requires a broker-assigned process pid\n');
1098
+ hooks.onStderr(textBytes('child_process: facet dispatch requires a broker-assigned process pid\n'));
1017
1099
  return 1;
1018
1100
  }
1019
1101
  payload = parsed.data;
1020
1102
  }
1021
1103
  catch {
1022
- hooks.onStderr('child_process: invalid facet dispatch payload\n');
1104
+ hooks.onStderr(textBytes('child_process: invalid facet dispatch payload\n'));
1023
1105
  return 1;
1024
1106
  }
1025
1107
  const registry = this._cpRegistry;
1026
1108
  if (!registry) {
1027
- hooks.onStderr('child_process: command registry unavailable\n');
1109
+ hooks.onStderr(textBytes('child_process: command registry unavailable\n'));
1028
1110
  return 127;
1029
1111
  }
1030
1112
  const commandName = normalizeCpCommandName(payload.command);
1031
1113
  const cmd = await registry.resolve(commandName);
1032
1114
  if (!cmd) {
1033
- hooks.onStderr(`${payload.command}: command not found\n`);
1115
+ hooks.onStderr(textBytes(`${payload.command}: command not found\n`));
1034
1116
  return 127;
1035
1117
  }
1036
1118
  // Synthesize a CommandContext for the internal shell substrate.
1037
- const stdoutStream = { write: (d) => hooks.onStdout(String(d)) };
1038
- const stderrStream = { write: (d) => hooks.onStderr(String(d)) };
1119
+ const stdoutStream = { write: (d) => hooks.onStdout(textBytes(String(d))) };
1120
+ const stderrStream = { write: (d) => hooks.onStderr(textBytes(String(d))) };
1039
1121
  const ac = new AbortController();
1040
1122
  const cred = this.processes.cred(payload.processPid);
1041
1123
  const ctx = {
@@ -1071,7 +1153,7 @@ export class NimbusSession extends CloudflareDurableObject {
1071
1153
  return typeof code === 'number' ? code : 0;
1072
1154
  }
1073
1155
  catch (e) {
1074
- hooks.onStderr(`${payload.command}: ${e?.message || String(e)}\n`);
1156
+ hooks.onStderr(textBytes(`${payload.command}: ${e?.message || String(e)}\n`));
1075
1157
  return 1;
1076
1158
  }
1077
1159
  },
@@ -1089,10 +1171,10 @@ export class NimbusSession extends CloudflareDurableObject {
1089
1171
  // construction near line 2058 — registry passed as ctor arg there).
1090
1172
  const cmdRegistryAdapter = {
1091
1173
  // Consult the live shell registry FIRST so dynamically-registered
1092
- // commands (registerUnixCommands / registerGitCommands / npm /
1093
- // wrangler etc.) are seen even if they're not in the static
1094
- // _CP_PURE_BUILTIN allow-list. Falls back to the static
1095
- // facet-direct table for known facet-only commands. Returns null
1174
+ // commands (registerUnixCommands / git / npm / wrangler etc.) are seen
1175
+ // even if they're not in the static _CP_PURE_BUILTIN allow-list. Falls
1176
+ // back to the static facet-direct table for known facet-only commands.
1177
+ // Returns null
1096
1178
  // (→ exit 127) for everything unknown.
1097
1179
  resolve: (name) => {
1098
1180
  const commandName = normalizeCpCommandName(name);
@@ -1108,13 +1190,13 @@ export class NimbusSession extends CloudflareDurableObject {
1108
1190
  runPureBuiltin: async (pid, name, args, env, cwd, stdin, hooks) => {
1109
1191
  const registry = this._cpRegistry;
1110
1192
  if (!registry) {
1111
- hooks.onStderr('cp: registry unavailable\n');
1193
+ hooks.onStderr(textBytes('cp: registry unavailable\n'));
1112
1194
  return 127;
1113
1195
  }
1114
1196
  const commandName = normalizeCpCommandName(name);
1115
1197
  const cmd = await registry.resolve(commandName);
1116
1198
  if (!cmd) {
1117
- hooks.onStderr(`${name}: command not found\n`);
1199
+ hooks.onStderr(textBytes(`${name}: command not found\n`));
1118
1200
  return 127;
1119
1201
  }
1120
1202
  const cred = this.processes.cred(pid);
@@ -1124,8 +1206,8 @@ export class NimbusSession extends CloudflareDurableObject {
1124
1206
  cred,
1125
1207
  args, env, cwd,
1126
1208
  vfs: this.sqliteFs.as(cred),
1127
- stdout: { write: (d) => hooks.onStdout(String(d)) },
1128
- stderr: { write: (d) => hooks.onStderr(String(d)) },
1209
+ stdout: { write: (d) => hooks.onStdout(textBytes(String(d))) },
1210
+ stderr: { write: (d) => hooks.onStderr(textBytes(String(d))) },
1129
1211
  signal: ac.signal,
1130
1212
  stdin: staticStdinReader(stdin),
1131
1213
  setUmask: (mask) => { this.processes.setUmask(pid, mask); },
@@ -1148,7 +1230,7 @@ export class NimbusSession extends CloudflareDurableObject {
1148
1230
  return typeof code === 'number' ? code : 0;
1149
1231
  }
1150
1232
  catch (e) {
1151
- hooks.onStderr(`${name}: ${e?.message || String(e)}\n`);
1233
+ hooks.onStderr(textBytes(`${name}: ${e?.message || String(e)}\n`));
1152
1234
  return 1;
1153
1235
  }
1154
1236
  },
@@ -1173,7 +1255,7 @@ export class NimbusSession extends CloudflareDurableObject {
1173
1255
  shellExecutor: {
1174
1256
  execute: async (pid, commandLine, env, cwd, stdin, hooks) => {
1175
1257
  if (!this.shell) {
1176
- hooks.onStderr('sh: shell unavailable\n');
1258
+ hooks.onStderr(textBytes('sh: shell unavailable\n'));
1177
1259
  return 127;
1178
1260
  }
1179
1261
  const cred = this.processes.cred(pid);
@@ -1194,8 +1276,8 @@ export class NimbusSession extends CloudflareDurableObject {
1194
1276
  const result = await this.shell.execute(String(commandLine), {
1195
1277
  cwd: cwd || '/home/user',
1196
1278
  env: { ...this.shell.env, ...(env || {}) },
1197
- onStdout: (d) => hooks.onStdout(String(d)),
1198
- onStderr: (d) => hooks.onStderr(String(d)),
1279
+ onStdout: hooks.onStdout,
1280
+ onStderr: hooks.onStderr,
1199
1281
  stdin,
1200
1282
  isolateShellState: true,
1201
1283
  commandContext: { pid, cred, setUmask },
@@ -1356,11 +1438,13 @@ export class NimbusSession extends CloudflareDurableObject {
1356
1438
  }
1357
1439
  this.npmInstaller = new NpmInstaller(this.sqliteFs, this.ctx.storage.sql, {
1358
1440
  esbuild: this.esbuildService,
1441
+ bundlePool: this.ensureBundlePool(),
1359
1442
  ctx: this.ctx,
1360
1443
  env: this.env,
1361
1444
  onProgress,
1362
1445
  fetchFn,
1363
1446
  });
1447
+ return this.npmInstaller;
1364
1448
  }
1365
1449
  /**
1366
1450
  * Read an environment flag with default-on semantics. Mirrors the
@@ -1385,7 +1469,7 @@ export class NimbusSession extends CloudflareDurableObject {
1385
1469
  // command registrations + boot wiring). The class retains `initSession`
1386
1470
  // as a delegator per plan §IX.4 R1. Visibility relaxed (was `private`)
1387
1471
  // so the SessionInternal interface declares it.
1388
- initSession(ws) {
1472
+ initSession(ws, options) {
1389
1473
  // A destroyed session id being legitimately re-initialized (shell WS
1390
1474
  // attach, or SDK ready via ensureProgrammaticReady which routes here)
1391
1475
  // lifts the tombstone so the recreated session's log-janitor can arm
@@ -1394,8 +1478,14 @@ export class NimbusSession extends CloudflareDurableObject {
1394
1478
  // Cast pattern (per plan §IX recommendation 1, used here only because
1395
1479
  // initSession reads this.ctx + this.env extensively; siblings that need
1396
1480
  // ctx/env take them as separate explicit args per DEFECT-D1).
1397
- return _w11InitSession(this, ws);
1481
+ return _w11InitSession(this, ws, options);
1398
1482
  }
1483
+ /**
1484
+ * The rebuild in flight for a shell socket that woke this instance from
1485
+ * hibernation, so every frame that arrives while it runs awaits the one
1486
+ * build instead of starting its own. See session/ws.ts bindShellSocket.
1487
+ */
1488
+ _wakeRebuild = null;
1399
1489
  // ── Filesystem seeding ────────────────────────────────────────────────
1400
1490
  ensureGlobalPrefixDirs(prefix) {
1401
1491
  const fs = this.sqliteFs.as(CRED_SESSION_USER);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The worker's half of `npm install`: core's `npm` command parses the
3
+ * invocation, owns the pre-checks and the summary text, and hands the
4
+ * install itself to this port. What the port owns is everything the
5
+ * command must not know: the NpmInstaller, the global prefix's directory
6
+ * shape, and the materialisation of bin shims into <prefix>/bin.
7
+ *
8
+ * `projectDir` is always the shell's cwd; `globalPrefix` (absolute VFS
9
+ * path, e.g. `/usr/local`) is present only for `npm install -g` and says
10
+ * where `<prefix>/lib/node_modules` and `<prefix>/bin` land. `pid` is the
11
+ * running command's pid — it authorizes the batch-facet writes.
12
+ */
13
+ import type { NpmInstallPort } from '@nimbus-sh/core/substrate/lifo/commands/system/npm.js';
14
+ import type { SessionInternal } from './internal.js';
15
+ export declare function createNpmInstallPort(self: SessionInternal): NpmInstallPort;
16
+ //# sourceMappingURL=npm-install-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"npm-install-port.d.ts","sourceRoot":"","sources":["../../src/session/npm-install-port.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uDAAuD,CAAC;AAI5F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,GAAG,cAAc,CAyC1E"}
@@ -0,0 +1,37 @@
1
+ import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
2
+ import { materializeNpmBinShims } from '../npm/bin-links.js';
3
+ export function createNpmInstallPort(self) {
4
+ return {
5
+ async install(spec) {
6
+ const globalPrefix = spec.global ? spec.globalPrefix : undefined;
7
+ const globalBinDir = globalPrefix ? `${globalPrefix}/bin` : undefined;
8
+ const sqliteFs = self.ensureSqliteFs();
9
+ const installer = await self.ensureNpmInstaller();
10
+ if (globalPrefix)
11
+ self.ensureGlobalPrefixDirs(globalPrefix);
12
+ const installCwd = globalPrefix ? `${globalPrefix}/lib` : spec.projectDir;
13
+ const result = await installer.install(installCwd, {
14
+ packages: spec.packages.length > 0 ? [...spec.packages] : undefined,
15
+ production: spec.production,
16
+ pid: spec.pid,
17
+ npmLog: spec.npmLog ?? undefined,
18
+ onProgress: spec.onProgress,
19
+ });
20
+ let linkedBins = 0;
21
+ if (globalPrefix && globalBinDir) {
22
+ // Materialise on-PATH bin shims even for partial installs — the
23
+ // bin linker already skips entries whose target never landed, so
24
+ // a partial tree safely exposes exactly the bins that installed.
25
+ const vfs = sqliteFs.as(CRED_KERNEL);
26
+ linkedBins = materializeNpmBinShims(vfs, `${installCwd}/node_modules`, globalBinDir);
27
+ }
28
+ return {
29
+ installed: result.installed,
30
+ failed: result.failed,
31
+ totalFiles: result.totalFiles,
32
+ fromCacheHits: result.cachedHits,
33
+ linkedBins,
34
+ };
35
+ },
36
+ };
37
+ }