@nimbus-sh/worker 0.4.0 → 0.6.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 (95) hide show
  1. package/README.md +46 -43
  2. package/dist/_shared/preview-host.d.ts +33 -2
  3. package/dist/_shared/preview-host.d.ts.map +1 -1
  4. package/dist/_shared/preview-host.js +73 -8
  5. package/dist/_shared/session-router.d.ts +43 -0
  6. package/dist/_shared/session-router.d.ts.map +1 -1
  7. package/dist/_shared/session-router.js +59 -12
  8. package/dist/facets/durable-images.d.ts +59 -0
  9. package/dist/facets/durable-images.d.ts.map +1 -0
  10. package/dist/facets/durable-images.js +94 -0
  11. package/dist/facets/durable-slots.d.ts +45 -0
  12. package/dist/facets/durable-slots.d.ts.map +1 -0
  13. package/dist/facets/durable-slots.js +95 -0
  14. package/dist/facets/esbuild-transform.d.ts +10 -0
  15. package/dist/facets/esbuild-transform.d.ts.map +1 -0
  16. package/dist/facets/esbuild-transform.js +36 -0
  17. package/dist/facets/manager.d.ts +244 -6
  18. package/dist/facets/manager.d.ts.map +1 -1
  19. package/dist/facets/manager.js +901 -117
  20. package/dist/facets/resident-identity.d.ts +23 -0
  21. package/dist/facets/resident-identity.d.ts.map +1 -0
  22. package/dist/facets/resident-identity.js +31 -0
  23. package/dist/git/commands.d.ts +22 -1
  24. package/dist/git/commands.d.ts.map +1 -1
  25. package/dist/git/commands.js +429 -425
  26. package/dist/git-bundle.generated.d.ts +2 -2
  27. package/dist/git-bundle.generated.d.ts.map +1 -1
  28. package/dist/git-bundle.generated.js +3 -3
  29. package/dist/index.d.ts +2 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -1
  32. package/dist/node-shims-artifact.generated.js +3 -3
  33. package/dist/router/index.d.ts.map +1 -1
  34. package/dist/router/index.js +97 -3
  35. package/dist/router/public-directory-do.d.ts +17 -0
  36. package/dist/router/public-directory-do.d.ts.map +1 -0
  37. package/dist/router/public-directory-do.js +16 -0
  38. package/dist/router/public-directory.d.ts +79 -0
  39. package/dist/router/public-directory.d.ts.map +1 -0
  40. package/dist/router/public-directory.js +122 -0
  41. package/dist/router/remote-api.d.ts +27 -1
  42. package/dist/router/remote-api.d.ts.map +1 -1
  43. package/dist/router/remote-api.js +48 -2
  44. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  45. package/dist/runtime/cpython-resident.js +3 -1
  46. package/dist/runtime/node-shims.d.ts +4 -1
  47. package/dist/runtime/node-shims.d.ts.map +1 -1
  48. package/dist/runtime/node-shims.js +324 -6
  49. package/dist/runtime/package-manager.d.ts +44 -1
  50. package/dist/runtime/package-manager.d.ts.map +1 -1
  51. package/dist/runtime/package-manager.js +168 -3
  52. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  53. package/dist/runtime/ruby-resident.js +2 -0
  54. package/dist/session/agent.d.ts +11 -0
  55. package/dist/session/agent.d.ts.map +1 -1
  56. package/dist/session/agent.js +28 -1
  57. package/dist/session/helpers.js +1 -1
  58. package/dist/session/init-phases.d.ts +17 -23
  59. package/dist/session/init-phases.d.ts.map +1 -1
  60. package/dist/session/init-phases.js +17 -41
  61. package/dist/session/init.d.ts.map +1 -1
  62. package/dist/session/init.js +28 -15
  63. package/dist/session/keys.d.ts +12 -0
  64. package/dist/session/keys.d.ts.map +1 -1
  65. package/dist/session/keys.js +12 -0
  66. package/dist/session/nimbus-session.d.ts +63 -5
  67. package/dist/session/nimbus-session.d.ts.map +1 -1
  68. package/dist/session/nimbus-session.js +131 -17
  69. package/dist/session/port-capability.d.ts +141 -8
  70. package/dist/session/port-capability.d.ts.map +1 -1
  71. package/dist/session/port-capability.js +265 -8
  72. package/dist/session/programmatic.d.ts +145 -3
  73. package/dist/session/programmatic.d.ts.map +1 -1
  74. package/dist/session/programmatic.js +434 -12
  75. package/dist/session/routes.d.ts +26 -0
  76. package/dist/session/routes.d.ts.map +1 -1
  77. package/dist/session/routes.js +117 -25
  78. package/dist/session/rpc.d.ts +24 -1
  79. package/dist/session/rpc.d.ts.map +1 -1
  80. package/dist/session/rpc.js +65 -98
  81. package/dist/session/shell-socket.d.ts +76 -0
  82. package/dist/session/shell-socket.d.ts.map +1 -0
  83. package/dist/session/shell-socket.js +172 -0
  84. package/dist/session/start-real-vite.js +1 -1
  85. package/dist/session/supervisor-op.d.ts +42 -0
  86. package/dist/session/supervisor-op.d.ts.map +1 -0
  87. package/dist/session/supervisor-op.js +96 -0
  88. package/dist/session/supervisor-rpc.d.ts +6 -10
  89. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  90. package/dist/session/supervisor-rpc.js +91 -71
  91. package/dist/session/ws.d.ts.map +1 -1
  92. package/dist/session/ws.js +19 -4
  93. package/package.json +4 -4
  94. package/public/_assets/runtime/{node-shims-fea906843f6d594e.js → node-shims-9e1380d9ee430fcc.js} +320 -5
  95. package/public/s/index.html +101 -5
@@ -21,13 +21,12 @@
21
21
  * these ~3 sites would each need ctx threaded through; cast at boundary
22
22
  * is acceptable per plan §IX recommendation 1.
23
23
  */
24
- import { enc, dec } from '@nimbus-sh/core/_shared/bytes.js';
24
+ import { enc } from '@nimbus-sh/core/_shared/bytes.js';
25
25
  import { normalizeTerminalNewlines } from '@nimbus-sh/core/_shared/terminal.js';
26
26
  import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
27
27
  import { getInnerDoClass } from '@nimbus-sh/fabric/inner-do-registry.js';
28
28
  import { NpmCache } from '../npm/cache.js';
29
29
  import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
30
- import { SqliteRuntimeFsBridge } from '@nimbus-sh/core/runtime/sqlite-runtime-fs-bridge.js';
31
30
  import { notifyTerminalEvent } from '../runtime/process-logs-api.js';
32
31
  import { IsolatePool } from '@nimbus-sh/fabric/isolate-pool.js';
33
32
  import { residentBootSpecSchema, } from '@nimbus-sh/fabric/process-fabric.js';
@@ -40,7 +39,6 @@ import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
40
39
  import { acquireSupervisorReadAllocation, } from '@nimbus-sh/platform/heavy-alloc-coord.js';
41
40
  import { rpcPayloadEnd, rpcPayloadStart, } from '@nimbus-sh/platform/diag-counters.js';
42
41
  import { CRED_KERNEL, CRED_SESSION_USER, } from '@nimbus-sh/core/runtime/os-contracts.js';
43
- import { getSymlinkRegistry } from '@nimbus-sh/core/vfs/symlink-registry.js';
44
42
  import { MAX_RPC_SAFE_PAYLOAD_BYTES } from '@nimbus-sh/platform/limits.js';
45
43
  import { FS_LIST_PAGE_LIMIT, FS_READ_BATCH_PATH_LIMIT, FS_READ_BATCH_REQUEST_BYTES, } from '@nimbus-sh/core/constants.js';
46
44
  import { routeSessionLoopback } from './loopback.js';
@@ -68,11 +66,6 @@ const WriteBatchPayloadSchema = z.object({
68
66
  chunks: z.array(WriteBatchChunkSchema).default([]),
69
67
  deletePaths: z.array(z.string()).optional(),
70
68
  }).passthrough();
71
- /**
72
- * Bridge key for the pid-less (host caller) filesystem view. ProcessTable
73
- * allocates pids from 1 upwards, so 0 can never collide with a process.
74
- */
75
- const HOST_CALLER_KEY = 0;
76
69
  function processPid(pid) {
77
70
  if (!Number.isInteger(pid) || typeof pid !== 'number' || pid <= 0) {
78
71
  throw new Error('filesystem RPC requires a valid process pid');
@@ -92,26 +85,7 @@ function processPid(pid) {
92
85
  function callerCred(self, pid) {
93
86
  return pid === undefined ? CRED_SESSION_USER : self.processes.cred(processPid(pid));
94
87
  }
95
- function processVfs(self, pid) {
96
- self.ensureSqliteFs();
97
- return self.sqliteFs.as(callerCred(self, pid));
98
- }
99
- function runtimeFs(self, pid) {
100
- const key = pid === undefined ? HOST_CALLER_KEY : processPid(pid);
101
- const vfs = processVfs(self, pid);
102
- if (!self.runtimeFsBridges)
103
- self.runtimeFsBridges = new Map();
104
- let bridge = self.runtimeFsBridges.get(key);
105
- if (!bridge) {
106
- bridge = new SqliteRuntimeFsBridge(vfs, self.sqliteFs);
107
- self.runtimeFsBridges.set(key, bridge);
108
- }
109
- else {
110
- bridge.updateCredential(vfs);
111
- }
112
- return bridge;
113
- }
114
- function checkedReadPayloadBytes(bytes) {
88
+ export function checkedReadPayloadBytes(bytes) {
115
89
  if (!Number.isSafeInteger(bytes) || bytes < 0) {
116
90
  throw new RangeError(`filesystem RPC read size must be a non-negative safe integer: ${bytes}`);
117
91
  }
@@ -129,13 +103,13 @@ function checkedReadPayloadBytes(bytes) {
129
103
  * `stat` here is a local SQLite lookup inside the DO — the same one
130
104
  * `_rpcReadFile` makes for the same reason — not a second round trip.
131
105
  */
132
- async function rangeReadBytes(fs, path, offset, length) {
106
+ export async function rangeReadBytes(fs, path, offset, length) {
133
107
  const stat = await fs.stat(path);
134
108
  if (!stat)
135
109
  return 0;
136
110
  return Math.max(0, Math.min(length, stat.size - offset));
137
111
  }
138
- async function withReadAllocation(bytes, read) {
112
+ export async function withReadAllocation(bytes, read) {
139
113
  const payloadBytes = checkedReadPayloadBytes(bytes);
140
114
  if (payloadBytes === 0)
141
115
  return read();
@@ -149,15 +123,13 @@ async function withReadAllocation(bytes, read) {
149
123
  lease.release();
150
124
  }
151
125
  }
126
+ /**
127
+ * `files.readFile` — the read with the supervisor's allocation lease.
128
+ * The body lives in `buildSessionSupervisorOps`'s `readFile` override so
129
+ * direct `_rpc*` calls and the supervisor envelope share it.
130
+ */
152
131
  export async function _rpcReadFile(self, path, pid) {
153
- const fs = runtimeFs(self, pid);
154
- const stat = await fs.stat(path);
155
- if (!stat)
156
- return null;
157
- return withReadAllocation(stat.size, async () => {
158
- const bytes = await fs.readFile(path);
159
- return bytes ? dec.decode(bytes) : null;
160
- });
132
+ return self.supervisorOp({ op: 'readFile', args: [path], pid });
161
133
  }
162
134
  /**
163
135
  * Read a file as raw bytes (Uint8Array). Used by git network facet for
@@ -165,11 +137,7 @@ export async function _rpcReadFile(self, path, pid) {
165
137
  * round-tripping through readFile (string) would corrupt bytes.
166
138
  */
167
139
  export async function _rpcReadFileBytes(self, path, pid) {
168
- const fs = runtimeFs(self, pid);
169
- const stat = await fs.stat(path);
170
- if (!stat)
171
- return null;
172
- return withReadAllocation(stat.size, () => fs.readFile(path));
140
+ return self.supervisorOp({ op: 'readFileBytes', args: [path], pid });
173
141
  }
174
142
  /**
175
143
  * Phase-3 inner-DO fetch dispatcher. Called by NimbusDOStub.fetch()
@@ -235,13 +203,10 @@ export async function _rpcInnerDoFetch(self, req) {
235
203
  }
236
204
  }
237
205
  export async function _rpcWriteFile(self, path, content, pid) {
238
- // binary-fs wave: SqliteVFS.writeFile already accepts string | Uint8Array
239
- // (sqlite-vfs.ts:937), so we forward the content shape unchanged. RPC
240
- // structured-clone preserves Uint8Array across the boundary; structured-
241
- // clone doesn't accept Buffer subclass instances, so fs.writeFileSync on
242
- // a Buffer flows through node-shims.ts:writeFileSync which stores it as
243
- // a plain Uint8Array on the cell — the shape that arrives here.
244
- return runtimeFs(self, pid).writeFile(path, content);
206
+ // binary-fs wave: the bridge's writeFile already accepts
207
+ // string | Uint8Array — the shape arrives unchanged across structured
208
+ // clone.
209
+ return self.supervisorOp({ op: 'writeFile', args: [path, content], pid });
245
210
  }
246
211
  /**
247
212
  * Write one host-governed file at a session root and let ordinary Unix
@@ -275,53 +240,52 @@ export async function _rpcWriteProtectedRootFile(self, rootPath, path, content)
275
240
  fs.chmod(protectedPath, 0o444);
276
241
  }
277
242
  export async function _rpcStat(self, path, pid) {
278
- return runtimeFs(self, pid).stat(path);
243
+ return self.supervisorOp({ op: 'stat', args: [path], pid });
279
244
  }
280
245
  export async function _rpcLstat(self, path, pid) {
281
- return runtimeFs(self, pid).stat(path, { followSymlinks: false });
246
+ return self.supervisorOp({ op: 'lstat', args: [path], pid });
282
247
  }
283
248
  export async function _rpcHasLegacySymlinkUnder(self, path, pid) {
284
- runtimeFs(self, pid);
285
- return getSymlinkRegistry(self.sqliteFs).hasAtOrBelow(path);
249
+ return self.supervisorOp({ op: 'hasLegacySymlinkUnder', args: [path], pid });
286
250
  }
287
251
  export async function _rpcUtimes(self, path, atimeMs, mtimeMs, pid) {
288
- await runtimeFs(self, pid).utimes(path, atimeMs, mtimeMs);
252
+ await self.supervisorOp({ op: 'utimes', args: [path, atimeMs, mtimeMs], pid });
289
253
  }
290
254
  export async function _rpcChmod(self, path, mode, pid) {
291
- await runtimeFs(self, pid).chmod(path, mode);
255
+ await self.supervisorOp({ op: 'chmod', args: [path, mode], pid });
292
256
  }
293
257
  export async function _rpcAccess(self, path, mode, pid) {
294
- await runtimeFs(self, pid).access(path, mode);
258
+ await self.supervisorBridge(pid).access(path, mode);
295
259
  }
296
260
  export async function _rpcChown(self, path, uid, gid, pid, options) {
297
- await runtimeFs(self, pid).chown(path, uid, gid, options);
261
+ await self.supervisorBridge(pid).chown(path, uid, gid, options);
298
262
  }
299
263
  export async function _rpcSetUmask(self, mask, pid) {
300
264
  return self.processes.setUmask(processPid(pid), mask);
301
265
  }
302
266
  export async function _rpcReaddir(self, path, pid) {
303
- return runtimeFs(self, pid).readdir(path);
267
+ return self.supervisorOp({ op: 'readdir', args: [path], pid });
304
268
  }
305
269
  export async function _rpcExists(self, path, pid) {
306
- return (await runtimeFs(self, pid).stat(path)) !== null;
270
+ return self.supervisorOp({ op: 'exists', args: [path], pid });
307
271
  }
308
272
  export async function _rpcMkdir(self, path, pid) {
309
- await runtimeFs(self, pid).mkdir(path, { recursive: true });
273
+ await self.supervisorOp({ op: 'mkdir', args: [path], pid });
310
274
  }
311
275
  export async function _rpcRmdir(self, path, pid) {
312
- await runtimeFs(self, pid).rmdir(path);
276
+ await self.supervisorOp({ op: 'rmdir', args: [path], pid });
313
277
  }
314
278
  export async function _rpcRename(self, from, to, pid) {
315
- await runtimeFs(self, pid).rename(from, to);
279
+ await self.supervisorOp({ op: 'rename', args: [from, to], pid });
316
280
  }
317
281
  export async function _rpcReadlink(self, path, pid) {
318
- return runtimeFs(self, pid).readlink(path);
282
+ return self.supervisorOp({ op: 'readlink', args: [path], pid });
319
283
  }
320
284
  export async function _rpcSymlink(self, target, path, pid) {
321
- await runtimeFs(self, pid).symlink(target, path);
285
+ await self.supervisorOp({ op: 'symlink', args: [target, path], pid });
322
286
  }
323
287
  const FsRangeOffsetSchema = z.number().int().min(0).finite();
324
- const FsReadRangeArgsSchema = z.object({
288
+ export const FsReadRangeArgsSchema = z.object({
325
289
  path: z.string(),
326
290
  offset: FsRangeOffsetSchema,
327
291
  length: FsRangeOffsetSchema,
@@ -366,7 +330,7 @@ const FsListArgsSchema = z.object({
366
330
  limit: z.number().int().min(1).max(FS_LIST_PAGE_LIMIT).nullable(),
367
331
  });
368
332
  export async function _rpcFsRevision(self, path, pid) {
369
- return runtimeFs(self, pid).revision(typeof path === 'string' ? path : undefined);
333
+ return self.supervisorOp({ op: 'fsRevision', args: [path], pid });
370
334
  }
371
335
  /**
372
336
  * The facet's WebSocket relay. A facet does not open its own sockets: the
@@ -416,23 +380,21 @@ export async function _rpcWsClose(self, id, code, reason, pid) {
416
380
  */
417
381
  export async function _rpcFsAcquire(self, epoch, cursor, pid) {
418
382
  const args = FsAcquireArgsSchema.parse({ epoch, cursor });
419
- return runtimeFs(self, pid).acquire(args.epoch, args.cursor);
383
+ return self.supervisorBridge(pid).acquire(args.epoch, args.cursor);
420
384
  }
421
385
  /**
422
386
  * Enumerate the session filesystem for a process, one bounded page at a time.
423
387
  *
424
- * Goes through `runtimeFs(self, pid)` like every other fs RPC, so the listing
388
+ * Goes through `self.supervisorBridge(pid)` like every other fs RPC, so the listing
425
389
  * is filtered by the calling process's own credential rather than the kernel's
426
390
  * — a process must not learn of a path it could not stat.
427
391
  */
428
392
  export async function _rpcFsList(self, after, limit, pid) {
429
393
  const args = FsListArgsSchema.parse({ after: after ?? null, limit: limit ?? null });
430
- return runtimeFs(self, pid).list(args.after, args.limit ?? undefined);
394
+ return self.supervisorBridge(pid).list(args.after, args.limit ?? undefined);
431
395
  }
432
396
  export async function _rpcFsReadRange(self, path, offset, length, pid) {
433
- const args = FsReadRangeArgsSchema.parse({ path, offset, length });
434
- const fs = runtimeFs(self, pid);
435
- return withReadAllocation(await rangeReadBytes(fs, args.path, args.offset, args.length), () => fs.readRange(args.path, args.offset, args.length));
397
+ return self.supervisorOp({ op: 'fsReadRange', args: [path, offset, length], pid });
436
398
  }
437
399
  /**
438
400
  * The same read, through the same process credential and the same bridge, with
@@ -449,9 +411,7 @@ export async function _rpcFsReadRange(self, path, offset, length, pid) {
449
411
  * substrate that was supposed to relieve the coordinator damages it instead.
450
412
  */
451
413
  export async function _rpcFsReadRangeUncached(self, path, offset, length, pid) {
452
- const args = FsReadRangeArgsSchema.parse({ path, offset, length });
453
- const fs = runtimeFs(self, pid);
454
- return withReadAllocation(await rangeReadBytes(fs, args.path, args.offset, args.length), () => fs.readRange(args.path, args.offset, args.length, { cached: false }));
414
+ return self.supervisorOp({ op: 'fsReadRangeUncached', args: [path, offset, length], pid });
455
415
  }
456
416
  /**
457
417
  * Read many ranges in ONE round trip.
@@ -481,7 +441,7 @@ export async function _rpcFsReadBatch(self, requests, pid) {
481
441
  // Sizing pass. A path this process may not stat contributes nothing and
482
442
  // still gets its own entry below — denying one path must not deny the
483
443
  // batch, which is what N separate reads would have done.
484
- const fs = runtimeFs(self, pid);
444
+ const fs = self.supervisorBridge(pid);
485
445
  let residentBytes = 0;
486
446
  for (const request of args) {
487
447
  try {
@@ -520,7 +480,7 @@ function readBatchEntryError(error) {
520
480
  }
521
481
  export async function _rpcFsWriteRange(self, path, offset, bytes, pid) {
522
482
  const args = FsWriteRangeArgsSchema.parse({ path, offset });
523
- return runtimeFs(self, pid).writeRange(args.path, args.offset, normalizeWriteBatchChunkData(bytes));
483
+ return self.supervisorBridge(pid).writeRange(args.path, args.offset, normalizeWriteBatchChunkData(bytes));
524
484
  }
525
485
  export async function _rpcFsAppend(self, path, writerId, moduleId, operationId, bytes, pid) {
526
486
  const args = FsAppendArgsSchema.parse({ path, writerId, moduleId, operationId });
@@ -532,7 +492,7 @@ export async function _rpcFsAppend(self, path, writerId, moduleId, operationId,
532
492
  const data = normalizeWriteBatchChunkData(bytes);
533
493
  const digestBytes = new Uint8Array(await crypto.subtle.digest('SHA-256', data));
534
494
  const digest = Array.from(digestBytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
535
- return runtimeFs(self, pid).appendOnce(args.path, processId, args.writerId, args.moduleId, sequence, digest, data);
495
+ return self.supervisorBridge(pid).appendOnce(args.path, processId, args.writerId, args.moduleId, sequence, digest, data);
536
496
  }
537
497
  export async function _rpcFsAppendAck(self, writerId, moduleId, operationId, pid) {
538
498
  const args = FsAppendAckArgsSchema.parse({ writerId, moduleId, operationId });
@@ -541,17 +501,16 @@ export async function _rpcFsAppendAck(self, writerId, moduleId, operationId, pid
541
501
  throw new Error('filesystem append operation exceeds the safe integer range');
542
502
  }
543
503
  const processId = processPid(pid);
544
- await runtimeFs(self, processId).acknowledgeAppend(processId, args.writerId, args.moduleId, sequence);
504
+ await self.supervisorBridge(processId).acknowledgeAppend(processId, args.writerId, args.moduleId, sequence);
545
505
  }
546
506
  export async function _rpcFsTruncate(self, path, size, pid) {
547
- const args = FsTruncateArgsSchema.parse({ path, size });
548
- await runtimeFs(self, pid).truncate(args.path, args.size);
507
+ await self.supervisorOp({ op: 'fsTruncate', args: [path, size], pid });
549
508
  }
550
509
  export async function _rpcFsOpen(self, path, flags, pid) {
551
- return runtimeFs(self, pid).open(path, flags || {});
510
+ return self.supervisorBridge(pid).open(path, flags || {});
552
511
  }
553
512
  export async function _rpcFsRead(self, handleId, offset, length, pid) {
554
- return withReadAllocation(length, () => runtimeFs(self, pid).read(handleId, offset, length));
513
+ return withReadAllocation(length, () => self.supervisorBridge(pid).read(handleId, offset, length));
555
514
  }
556
515
  export async function _rpcFsWrite(self, handleId, offset, bytes, pid) {
557
516
  let data;
@@ -561,10 +520,10 @@ export async function _rpcFsWrite(self, handleId, offset, bytes, pid) {
561
520
  data = new Uint8Array(bytes);
562
521
  else
563
522
  data = new Uint8Array(bytes || []);
564
- return runtimeFs(self, pid).write(handleId, offset, data);
523
+ return self.supervisorBridge(pid).write(handleId, offset, data);
565
524
  }
566
525
  export async function _rpcFsClose(self, handleId, pid) {
567
- await runtimeFs(self, pid).close(handleId);
526
+ await self.supervisorBridge(pid).close(handleId);
568
527
  }
569
528
  /**
570
529
  * Called by CirrusHmrRPC.hmrSend. Runs in the DO's own context so
@@ -577,7 +536,7 @@ export async function _rpcHmrRelay(self, clientId, msg) {
577
536
  self.cirrusReal.hmr.relayToBrowser(clientId, msg);
578
537
  }
579
538
  export async function _rpcUnlink(self, path, pid) {
580
- await runtimeFs(self, pid).unlink(path);
539
+ await self.supervisorOp({ op: 'unlink', args: [path], pid });
581
540
  }
582
541
  /**
583
542
  * Bulk-write files and directories via one transactionSync().
@@ -601,7 +560,7 @@ export async function _rpcWriteBatch(self, payload, pid) {
601
560
  chunkId: c.chunkId,
602
561
  data: normalizeWriteBatchChunkData(c.data),
603
562
  }));
604
- return processVfs(self, pid).writeBatch({
563
+ return self.sqliteFs.as(callerCred(self, pid)).writeBatch({
605
564
  inodes,
606
565
  chunks,
607
566
  deletePaths,
@@ -660,10 +619,8 @@ export async function _rpcWriteBatchStream(self, stream, mutationOwner, pid) {
660
619
  // total writeBatchStream RPCs at the coordinator (vs 620+ pre-fix).
661
620
  // Workerd's input-gate queue depth on the coordinator stays well
662
621
  // under the queue-age threshold without any user-space semaphore.
663
- const decodeDrainStartedAt = performance.now();
664
- return processVfs(self, pid).writeStream(stream, {
665
- decodeDrainStartedAt,
666
- mutationOwner,
622
+ return self.supervisorOp({
623
+ op: 'writeBatchStream', stream, mutationOwner, pid,
667
624
  });
668
625
  }
669
626
  /**
@@ -789,7 +746,7 @@ export async function _rpcReportExit(self, pid, code, tail) {
789
746
  self.webSocketRelay?.closeForPid(pid);
790
747
  }
791
748
  catch { }
792
- self.runtimeFsBridges?.delete(pid);
749
+ self.supervisorForgetBridge?.(pid);
793
750
  if (tail)
794
751
  self.processes.appendOutput(pid, 'stderr', tail);
795
752
  // Guard against double-reporting: if we've already recorded exit
@@ -951,7 +908,7 @@ export function _reportExternalExit(self, pid, code, reason) {
951
908
  self.webSocketRelay?.closeForPid(pid);
952
909
  }
953
910
  catch { }
954
- self.runtimeFsBridges?.delete(pid);
911
+ self.supervisorForgetBridge?.(pid);
955
912
  if (reason) {
956
913
  self.processes.appendOutput(pid, 'stderr', `[process killed: ${reason}]\n`);
957
914
  }
@@ -1009,9 +966,17 @@ export async function _rpcPrefetch(self, cwd, entryCode) {
1009
966
  return prefetchForRequire(self.sqliteFs, entryCode, cwd).bundle;
1010
967
  }
1011
968
  export async function _rpcRegisterPort(self, pid, port) {
1012
- // Port registration stores the facet association
1013
- // The actual facet stub is stored by FacetManager separately
1014
- // A new registration retires the previous occupant's preview capability.
969
+ // Port registration stores the facet association — the actual facet stub
970
+ // is stored by FacetManager separately. Registration goes through the
971
+ // manager so a pid binding a reserved port inherits the reservation's
972
+ // durable contract: its journal row is stamped with the port's owner and
973
+ // the stored capability is re-adopted rather than retired.
974
+ if (self.facetManager) {
975
+ await self.facetManager.registerPort(pid, port);
976
+ return;
977
+ }
978
+ // No facet manager yet means nothing resident is running — a bare
979
+ // registration retires the previous occupant's preview capability.
1015
980
  await clearPortCapability(self, port);
1016
981
  self.portRegistry.register(port, pid);
1017
982
  }
@@ -1027,7 +992,9 @@ export async function _rpcRouteLoopback(self, port, request) {
1027
992
  export async function _rpcTransform(self, code, loader) {
1028
993
  if (!self.esbuildService) {
1029
994
  self.ensureSqliteFs();
1030
- self.esbuildService = new EsbuildService(self.sqliteFs);
995
+ if (!self.sqliteFs)
996
+ throw new Error('Session VFS is not initialized');
997
+ self.esbuildService = new EsbuildService(self.sqliteFs.as(CRED_KERNEL));
1031
998
  }
1032
999
  try {
1033
1000
  const result = await self.esbuildService.transform(code, {
@@ -0,0 +1,76 @@
1
+ /**
2
+ * src/session/shell-socket.ts — who owns the session's terminal.
3
+ *
4
+ * The /ws upgrade refuses a second browser terminal on one session, so it
5
+ * has to know whether the terminal already has an owner. `readyState ===
6
+ * OPEN` does not answer that. It describes this end of the socket. A tab
7
+ * that dies without a close frame leaves a socket that still reads OPEN
8
+ * with nobody on the other end, and an upgrade that fails after accepting
9
+ * its socket leaves one whose client half was never handed to anyone. No
10
+ * close frame can ever arrive for either. Refusing every reconnect
11
+ * against such a socket locks the session out for good.
12
+ *
13
+ * Ownership needs peer-originated evidence, and that evidence has to
14
+ * outlive a hibernation cycle, because isolate memory does not. Two
15
+ * sources qualify:
16
+ *
17
+ * 1. `seenAt` in the socket's attachment. The attachment survives
18
+ * hibernation. Every inbound frame refreshes it, rate-limited to one
19
+ * write per STAMP_INTERVAL_MS, so a fast typist costs one attachment
20
+ * write per 15 s.
21
+ * 2. `ctx.getWebSocketAutoResponseTimestamp(ws)`. The runtime answers
22
+ * the configured `ping` itself without waking the object, so a ping
23
+ * never reaches case 1 and leaves no other trace. The browser
24
+ * terminal does not ping — it probes with an ordinary frame, for
25
+ * the reasons public/s/index.html gives — so this covers the other
26
+ * clients rather than that one.
27
+ *
28
+ * A close or an error is better than either: the runtime is telling the
29
+ * object outright that this socket is finished, so `clearShellSocketStamp`
30
+ * drops the evidence there and the next upgrade needs no inference at all.
31
+ *
32
+ * A socket owns the terminal while the later of the two falls inside
33
+ * SHELL_OWNER_LIVENESS_MS. The window has to cover the longest silence a
34
+ * live tab can produce. Browsers throttle a background tab's timers to
35
+ * about one tick per minute, so 120 s clears that with margin. A tab
36
+ * quiet for longer has stopped running its own code, and the upgrade that
37
+ * takes the terminal also closes its socket — so it redials cleanly
38
+ * instead of holding a terminal it can no longer use.
39
+ */
40
+ /** How long a shell socket owns the terminal after its peer was heard. */
41
+ export declare const SHELL_OWNER_LIVENESS_MS = 120000;
42
+ /** The part of `DurableObjectState` this module reads. */
43
+ export interface AutoResponseHost {
44
+ getWebSocketAutoResponseTimestamp?(ws: WebSocket): Date | null;
45
+ }
46
+ /** Tag a freshly accepted terminal socket as this session's shell owner. */
47
+ export declare function tagShellSocket(ws: WebSocket, now?: number): void;
48
+ /**
49
+ * Drop the evidence that a peer was on this socket.
50
+ *
51
+ * Called when the runtime reports the socket closed or errored. Both say
52
+ * this socket is finished, so the next upgrade must not read a stamp the
53
+ * dying handler wrote moments earlier and refuse the reconnect. Keeps the
54
+ * kind, so the socket still classifies as the terminal's while it drains.
55
+ * A peer that turns out to be alive re-stamps on its next frame.
56
+ */
57
+ export declare function clearShellSocketStamp(ws: WebSocket): void;
58
+ /**
59
+ * Record that the peer on this socket is still there.
60
+ *
61
+ * Called on every inbound shell frame. Rewrites the attachment only once
62
+ * the recorded stamp has aged past STAMP_INTERVAL_MS. No-op on sockets
63
+ * that carry another kind.
64
+ */
65
+ export declare function noteShellSocketActivity(ws: WebSocket, now?: number): void;
66
+ /** True when another browser terminal currently holds this session. */
67
+ export declare function hasLiveShellOwner(ctx: AutoResponseHost | undefined, sockets: readonly WebSocket[], now?: number): boolean;
68
+ /**
69
+ * Close the shell sockets no peer holds any more.
70
+ *
71
+ * Called on an upgrade that is taking the terminal. Without it, a socket
72
+ * whose tab is gone stays accepted for the life of the object, and every
73
+ * warm rejoin adds another one.
74
+ */
75
+ export declare function closeStaleShellSockets(ctx: AutoResponseHost | undefined, sockets: readonly WebSocket[], now?: number): void;
76
+ //# sourceMappingURL=shell-socket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell-socket.d.ts","sourceRoot":"","sources":["../../src/session/shell-socket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,SAAU,CAAC;AAU/C,0DAA0D;AAC1D,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC,CAAC,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC;CAChE;AAwFD,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI,CAE5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAGzD;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI,CAKrF;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,GAAG,SAAS,EACjC,OAAO,EAAE,SAAS,SAAS,EAAE,EAC7B,GAAG,GAAE,MAAmB,GACvB,OAAO,CAET;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,gBAAgB,GAAG,SAAS,EACjC,OAAO,EAAE,SAAS,SAAS,EAAE,EAC7B,GAAG,GAAE,MAAmB,GACvB,IAAI,CAKN"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * src/session/shell-socket.ts — who owns the session's terminal.
3
+ *
4
+ * The /ws upgrade refuses a second browser terminal on one session, so it
5
+ * has to know whether the terminal already has an owner. `readyState ===
6
+ * OPEN` does not answer that. It describes this end of the socket. A tab
7
+ * that dies without a close frame leaves a socket that still reads OPEN
8
+ * with nobody on the other end, and an upgrade that fails after accepting
9
+ * its socket leaves one whose client half was never handed to anyone. No
10
+ * close frame can ever arrive for either. Refusing every reconnect
11
+ * against such a socket locks the session out for good.
12
+ *
13
+ * Ownership needs peer-originated evidence, and that evidence has to
14
+ * outlive a hibernation cycle, because isolate memory does not. Two
15
+ * sources qualify:
16
+ *
17
+ * 1. `seenAt` in the socket's attachment. The attachment survives
18
+ * hibernation. Every inbound frame refreshes it, rate-limited to one
19
+ * write per STAMP_INTERVAL_MS, so a fast typist costs one attachment
20
+ * write per 15 s.
21
+ * 2. `ctx.getWebSocketAutoResponseTimestamp(ws)`. The runtime answers
22
+ * the configured `ping` itself without waking the object, so a ping
23
+ * never reaches case 1 and leaves no other trace. The browser
24
+ * terminal does not ping — it probes with an ordinary frame, for
25
+ * the reasons public/s/index.html gives — so this covers the other
26
+ * clients rather than that one.
27
+ *
28
+ * A close or an error is better than either: the runtime is telling the
29
+ * object outright that this socket is finished, so `clearShellSocketStamp`
30
+ * drops the evidence there and the next upgrade needs no inference at all.
31
+ *
32
+ * A socket owns the terminal while the later of the two falls inside
33
+ * SHELL_OWNER_LIVENESS_MS. The window has to cover the longest silence a
34
+ * live tab can produce. Browsers throttle a background tab's timers to
35
+ * about one tick per minute, so 120 s clears that with margin. A tab
36
+ * quiet for longer has stopped running its own code, and the upgrade that
37
+ * takes the terminal also closes its socket — so it redials cleanly
38
+ * instead of holding a terminal it can no longer use.
39
+ */
40
+ /** How long a shell socket owns the terminal after its peer was heard. */
41
+ export const SHELL_OWNER_LIVENESS_MS = 120_000;
42
+ /** Smallest gap between two attachment writes on one socket. */
43
+ const STAMP_INTERVAL_MS = 15_000;
44
+ /** Tolerance for a `seenAt` that reads ahead of the current clock. */
45
+ const CLOCK_SKEW_MS = 60_000;
46
+ const SHELL_KIND = 'shell';
47
+ function readAttachment(ws) {
48
+ try {
49
+ const read = Reflect.get(ws, 'deserializeAttachment');
50
+ if (typeof read !== 'function')
51
+ return null;
52
+ return Reflect.apply(read, ws, []);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ function writeAttachment(ws, attachment) {
59
+ try {
60
+ const write = Reflect.get(ws, 'serializeAttachment');
61
+ if (typeof write === 'function')
62
+ Reflect.apply(write, ws, [attachment]);
63
+ }
64
+ catch { /* the socket is closing, or the runtime refused the write */ }
65
+ }
66
+ /**
67
+ * Read a shell socket's stamp in one attachment pass.
68
+ *
69
+ * Returns null when the socket carries another kind, and a stamp of null
70
+ * when it is a shell socket nothing has stamped yet. An attachment
71
+ * outlives the deploy that wrote it and is untrusted on the way back, so
72
+ * a stamp that is not a plain timestamp, or that reads further ahead than
73
+ * the clocks can differ, counts as no stamp at all.
74
+ */
75
+ function readShellStamp(ws, now) {
76
+ const attachment = readAttachment(ws);
77
+ if (typeof attachment !== 'object' || attachment === null)
78
+ return null;
79
+ const record = attachment;
80
+ if (record.kind !== SHELL_KIND)
81
+ return null;
82
+ const seenAt = record.seenAt;
83
+ if (typeof seenAt !== 'number' || !Number.isFinite(seenAt) || seenAt > now + CLOCK_SKEW_MS) {
84
+ return { seenAt: null };
85
+ }
86
+ return { seenAt };
87
+ }
88
+ /** When the runtime last answered a ping on this socket, if it ever did. */
89
+ function autoResponseAt(ctx, ws, now) {
90
+ // Null whenever the runtime has never auto-answered on this socket.
91
+ const at = ctx?.getWebSocketAutoResponseTimestamp?.(ws) ?? null;
92
+ if (!at)
93
+ return null;
94
+ const ms = at.getTime();
95
+ if (!Number.isFinite(ms) || ms > now + CLOCK_SKEW_MS)
96
+ return null;
97
+ return ms;
98
+ }
99
+ /** True while a peer is still proven to be on this shell socket. */
100
+ function ownsTerminal(ctx, ws, stamp, now) {
101
+ const answered = autoResponseAt(ctx, ws, now);
102
+ const heard = stamp.seenAt === null
103
+ ? answered
104
+ : (answered === null ? stamp.seenAt : Math.max(stamp.seenAt, answered));
105
+ return heard !== null && now - heard < SHELL_OWNER_LIVENESS_MS;
106
+ }
107
+ /** The open shell sockets, each with the stamp it carries. */
108
+ function openShellSockets(sockets, now) {
109
+ const found = [];
110
+ for (const ws of sockets) {
111
+ if (ws.readyState !== WebSocket.OPEN)
112
+ continue;
113
+ const stamp = readShellStamp(ws, now);
114
+ if (stamp)
115
+ found.push({ ws, stamp });
116
+ }
117
+ return found;
118
+ }
119
+ /** Tag a freshly accepted terminal socket as this session's shell owner. */
120
+ export function tagShellSocket(ws, now = Date.now()) {
121
+ writeAttachment(ws, { kind: SHELL_KIND, seenAt: now });
122
+ }
123
+ /**
124
+ * Drop the evidence that a peer was on this socket.
125
+ *
126
+ * Called when the runtime reports the socket closed or errored. Both say
127
+ * this socket is finished, so the next upgrade must not read a stamp the
128
+ * dying handler wrote moments earlier and refuse the reconnect. Keeps the
129
+ * kind, so the socket still classifies as the terminal's while it drains.
130
+ * A peer that turns out to be alive re-stamps on its next frame.
131
+ */
132
+ export function clearShellSocketStamp(ws) {
133
+ if (readShellStamp(ws, Date.now()) === null)
134
+ return;
135
+ writeAttachment(ws, { kind: SHELL_KIND });
136
+ }
137
+ /**
138
+ * Record that the peer on this socket is still there.
139
+ *
140
+ * Called on every inbound shell frame. Rewrites the attachment only once
141
+ * the recorded stamp has aged past STAMP_INTERVAL_MS. No-op on sockets
142
+ * that carry another kind.
143
+ */
144
+ export function noteShellSocketActivity(ws, now = Date.now()) {
145
+ const stamp = readShellStamp(ws, now);
146
+ if (!stamp)
147
+ return;
148
+ if (stamp.seenAt !== null && now - stamp.seenAt < STAMP_INTERVAL_MS)
149
+ return;
150
+ writeAttachment(ws, { kind: SHELL_KIND, seenAt: now });
151
+ }
152
+ /** True when another browser terminal currently holds this session. */
153
+ export function hasLiveShellOwner(ctx, sockets, now = Date.now()) {
154
+ return openShellSockets(sockets, now).some(({ ws, stamp }) => ownsTerminal(ctx, ws, stamp, now));
155
+ }
156
+ /**
157
+ * Close the shell sockets no peer holds any more.
158
+ *
159
+ * Called on an upgrade that is taking the terminal. Without it, a socket
160
+ * whose tab is gone stays accepted for the life of the object, and every
161
+ * warm rejoin adds another one.
162
+ */
163
+ export function closeStaleShellSockets(ctx, sockets, now = Date.now()) {
164
+ for (const { ws, stamp } of openShellSockets(sockets, now)) {
165
+ if (ownsTerminal(ctx, ws, stamp, now))
166
+ continue;
167
+ try {
168
+ ws.close(1001, 'terminal taken over');
169
+ }
170
+ catch { /* already closing */ }
171
+ }
172
+ }
@@ -53,7 +53,7 @@ export async function startRealVite(self, opts) {
53
53
  if (cfgPath) {
54
54
  try {
55
55
  if (!self.esbuildService)
56
- self.esbuildService = new EsbuildService(self.sqliteFs);
56
+ self.esbuildService = new EsbuildService(kernelFs);
57
57
  const bundleResult = await self.esbuildService.build([cfgPath], {
58
58
  bundle: true,
59
59
  format: 'esm',