@nimbus-sh/worker 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/loaders/generated-workers.d.ts +2 -2
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +3 -3
- package/dist/runtime/facet-loader-host.d.ts +3 -2
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +1 -0
- package/dist/runtime/ruby-repl.js +1 -1
- package/dist/runtime/ruby-resident.d.ts +20 -0
- package/dist/runtime/ruby-resident.d.ts.map +1 -0
- package/dist/runtime/ruby-resident.js +278 -0
- package/dist/session/init.d.ts +16 -7
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +181 -208
- package/dist/session/nimbus-session.d.ts +11 -1
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +13 -57
- package/dist/session/programmatic.d.ts +1 -1
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +1 -1
- package/dist/session/routes.js +1 -1
- package/package.json +2 -2
- package/scripts/bundle-facet-workers.mjs +4 -3
- package/scripts/bundle-runtime.mjs +276 -44
- package/dist/npm/tarball-stream.d.ts +0 -93
- package/dist/npm/tarball-stream.d.ts.map +0 -1
- package/dist/npm/tarball-stream.js +0 -235
- package/dist/npm/tarball.d.ts +0 -34
- package/dist/npm/tarball.d.ts.map +0 -1
- package/dist/npm/tarball.js +0 -97
- package/dist/runtime/clang-runner.d.ts +0 -37
- package/dist/runtime/clang-runner.d.ts.map +0 -1
- package/dist/runtime/clang-runner.js +0 -865
- package/dist/runtime/ruby-gems.d.ts +0 -30
- package/dist/runtime/ruby-gems.d.ts.map +0 -1
- package/dist/runtime/ruby-gems.js +0 -636
- package/dist/runtime/ruby-runner.d.ts +0 -75
- package/dist/runtime/ruby-runner.d.ts.map +0 -1
- package/dist/runtime/ruby-runner.js +0 -1577
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* generated-workers.ts — AUTO-GENERATED. DO NOT EDIT.
|
|
3
3
|
*
|
|
4
4
|
* Produced by scripts/bundle-facet-workers.mjs from:
|
|
5
|
-
* - src/
|
|
5
|
+
* - @nimbus-sh/core src/_shared/tarball-stream.ts (streaming tar primitives)
|
|
6
6
|
* - @nimbus-sh/core src/_shared/w7-frame.ts (W7 streaming bulk-write encoder)
|
|
7
7
|
*
|
|
8
8
|
* Consumed by src/loaders/loader-pool.ts callers via the `preamble`
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* W7-frame symbols: encodeWriteBatchStream, decodeWriteBatchStream,
|
|
16
16
|
* W7_MAGIC, W7_MAX_RECORD_BYTES.
|
|
17
17
|
*
|
|
18
|
-
* Tar size: 4.
|
|
18
|
+
* Tar size: 4.15 KiB
|
|
19
19
|
* W7 size: 29.18 KiB
|
|
20
20
|
*/
|
|
21
21
|
export declare const TAR_STREAM_PREAMBLE: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generated-workers.d.ts","sourceRoot":"","sources":["../../src/loaders/generated-workers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,mBAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"generated-workers.d.ts","sourceRoot":"","sources":["../../src/loaders/generated-workers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,mBAAmB,EAAE,MAA+1I,CAAC;AAEl4I,eAAO,MAAM,iBAAiB,EAAE,MAAyt8B,CAAC"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* generated-workers.ts — AUTO-GENERATED. DO NOT EDIT.
|
|
3
3
|
*
|
|
4
4
|
* Produced by scripts/bundle-facet-workers.mjs from:
|
|
5
|
-
* - src/
|
|
5
|
+
* - @nimbus-sh/core src/_shared/tarball-stream.ts (streaming tar primitives)
|
|
6
6
|
* - @nimbus-sh/core src/_shared/w7-frame.ts (W7 streaming bulk-write encoder)
|
|
7
7
|
*
|
|
8
8
|
* Consumed by src/loaders/loader-pool.ts callers via the `preamble`
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* W7-frame symbols: encodeWriteBatchStream, decodeWriteBatchStream,
|
|
16
16
|
* W7_MAGIC, W7_MAX_RECORD_BYTES.
|
|
17
17
|
*
|
|
18
|
-
* Tar size: 4.
|
|
18
|
+
* Tar size: 4.15 KiB
|
|
19
19
|
* W7 size: 29.18 KiB
|
|
20
20
|
*/
|
|
21
|
-
export const TAR_STREAM_PREAMBLE = "// src/
|
|
21
|
+
export const TAR_STREAM_PREAMBLE = "// ../core/src/_shared/tarball-stream.ts\nvar MAX_FILE_BYTES = 2e7;\nfunction canonicalTarName(name) {\n const out = [];\n for (const seg of name.split(\"/\")) {\n if (seg === \"..\") {\n if (out.length === 0) return \"\";\n out.pop();\n } else if (seg !== \"\" && seg !== \".\") {\n out.push(seg);\n }\n }\n return out.join(\"/\");\n}\nfunction parseTarHeader(block) {\n if (block[0] === 0) return null;\n let name = \"\";\n for (let i = 0; i < 100 && block[i] !== 0; i++) {\n name += String.fromCharCode(block[i]);\n }\n let prefix = \"\";\n for (let i = 345; i < 500 && block[i] !== 0; i++) {\n prefix += String.fromCharCode(block[i]);\n }\n if (prefix) name = prefix + \"/\" + name;\n name = name.replace(/^package\\//, \"\");\n name = canonicalTarName(name);\n let sizeStr = \"\";\n for (let i = 124; i < 136 && block[i] !== 0; i++) {\n sizeStr += String.fromCharCode(block[i]);\n }\n const size = parseInt(sizeStr.trim(), 8) || 0;\n const typeFlag = block[156];\n return { name, size, typeFlag };\n}\nasync function* readableStreamToAsyncIterable(rs) {\n const reader = rs.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) return;\n if (value && value.length > 0) yield value;\n }\n } finally {\n try {\n reader.releaseLock();\n } catch {\n }\n }\n}\nasync function* streamTarEntries(source, onSkip) {\n let carry = new Uint8Array(0);\n let state = { kind: \"header\" };\n function concat(a, b) {\n if (a.length === 0) return b;\n if (b.length === 0) return a;\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n }\n for await (const chunkRaw of source) {\n let buf = concat(carry, chunkRaw);\n let cursor = 0;\n while (true) {\n if (state.kind === \"header\") {\n if (buf.length - cursor < 512) break;\n const header = buf.subarray(cursor, cursor + 512);\n const parsed = parseTarHeader(header);\n cursor += 512;\n if (!parsed) return;\n const { name, size, typeFlag } = parsed;\n const pad = size === 0 ? 0 : (512 - size % 512) % 512;\n const isRegularFile = typeFlag === 48 || typeFlag === 0;\n if (size === 0) {\n if (isRegularFile && name) {\n yield { name, data: new Uint8Array(0) };\n }\n state = { kind: \"header\" };\n continue;\n }\n if (!isRegularFile || !name || size > MAX_FILE_BYTES) {\n if (onSkip) {\n const reason = !isRegularFile ? \"non-regular\" : !name ? \"no-name\" : \"too-large\";\n try {\n onSkip(name, size, reason);\n } catch {\n }\n }\n state = { kind: \"skip\", remaining: size + pad };\n continue;\n }\n state = {\n kind: \"file\",\n name,\n remaining: size,\n fileBuf: new Uint8Array(size),\n fileOffset: 0,\n pad,\n skip: false\n };\n continue;\n }\n if (state.kind === \"file\") {\n const avail2 = buf.length - cursor;\n if (avail2 === 0) break;\n if (state.remaining > 0) {\n const take2 = Math.min(state.remaining, avail2);\n state.fileBuf.set(buf.subarray(cursor, cursor + take2), state.fileOffset);\n state.fileOffset += take2;\n state.remaining -= take2;\n cursor += take2;\n if (state.remaining > 0) break;\n }\n if (state.pad > 0) {\n const avail22 = buf.length - cursor;\n if (avail22 === 0) break;\n const take2 = Math.min(state.pad, avail22);\n state.pad -= take2;\n cursor += take2;\n if (state.pad > 0) break;\n }\n yield { name: state.name, data: state.fileBuf };\n state = { kind: \"header\" };\n continue;\n }\n const avail = buf.length - cursor;\n if (avail === 0) break;\n const take = Math.min(state.remaining, avail);\n cursor += take;\n state.remaining -= take;\n if (state.remaining > 0) break;\n state = { kind: \"header\" };\n }\n if (cursor >= buf.length) {\n carry = new Uint8Array(0);\n } else if (cursor === 0) {\n carry = buf;\n } else {\n carry = buf.slice(cursor);\n }\n }\n}";
|
|
22
22
|
export const W7_FRAME_PREAMBLE = "// ../core/src/constants.ts\nvar CHUNK_SIZE = 65536;\nvar VFS_CAPACITY = 10 * 1024 * 1024 * 1024;\nvar MAX_TX_BLOB_BYTES = 1 * 1024 * 1024;\nvar MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES = 8 * 1024 * 1024;\nvar MAX_RPC_SAFE_PAYLOAD_BYTES = 28 * 1024 * 1024;\nvar FS_READ_BATCH_REQUEST_BYTES = 4 * 1024 * 1024;\nvar VFS_BUNDLE_MAX_BYTES = 24 * 1024 * 1024;\nvar BUNDLE_MAX_ENCODED_BYTES = 22 * 1024 * 1024;\nvar PREFETCH_CACHE_MAX_BYTES = 16 * 1024 * 1024;\nvar CWD_SNAPSHOT_MAX_FILE_BYTES = 2 * 1024 * 1024;\nvar SUPERVISOR_HEAP_CEILING_BYTES = 64 * 1024 * 1024;\nvar SUPERVISOR_IN_FLIGHT_ALLOCATION_BUDGET_BYTES = 40 * 1024 * 1024;\nvar SUPERVISOR_READ_RESERVE_BYTES = 1024 * 1024;\n\n// ../core/src/_shared/w7-frame.ts\nvar W7_MAGIC = new Uint8Array([78, 87, 55, 3]);\nvar ENCODER_QUEUE_HWM = 0;\nvar MAX_METADATA_BYTES = 64 * 1024;\nvar MAX_PATH_BYTES = 64 * 1024;\nvar MAX_BATCH_ID_BYTES = 128;\nvar MAX_CONTENT_ID_BYTES = 256;\nvar W7_MAX_PATHS_PER_BATCH = 128;\nvar W7_MAX_OWNED_PATH_BYTES = 64 * 1024;\nvar W7_MAX_RECORD_BYTES = 5 + 4 + MAX_CONTENT_ID_BYTES + 8 + CHUNK_SIZE;\nvar MODE = \"path-atomic-committed-prefix\";\nfunction encodeWriteBatchStream(payload) {\n const batchId = crypto.randomUUID();\n const { deletes, directories, files } = preparePayload(payload, batchId);\n const iterator = encodeRecords(batchId, deletes, directories, files);\n let closed = false;\n let magicEmitted = false;\n const source = {\n type: \"bytes\",\n pull(controller) {\n if (closed) return;\n try {\n if (!magicEmitted) {\n magicEmitted = true;\n controller.enqueue(W7_MAGIC.slice());\n return;\n }\n const next = iterator.next();\n if (next.done) {\n closed = true;\n controller.close();\n return;\n }\n for (const part of next.value) controller.enqueue(part);\n } catch (error) {\n closed = true;\n controller.error(error);\n }\n },\n cancel() {\n closed = true;\n iterator.return?.(void 0);\n }\n };\n return new ReadableStream(source, {\n highWaterMark: ENCODER_QUEUE_HWM\n });\n}\nasync function decodeWriteBatchStream(stream, options = {}) {\n let reader;\n try {\n reader = stream.getReader({ mode: \"byob\" });\n } catch {\n throw new Error(\"w7-frame: stream must be a byte-oriented ReadableStream\");\n }\n const buffer = new ExactByteReader(reader);\n let handedOff = false;\n try {\n throwIfAborted(options.signal);\n const magic = await buffer.readExact(W7_MAGIC.length, \"magic\");\n if (!bytesEqual(magic, W7_MAGIC)) {\n const version = magic.length === 4 && magic[0] === 78 && magic[1] === 87 && magic[2] === 55 ? magic[3] : null;\n if (version !== null) {\n throw new Error(`w7-frame: unsupported protocol version ${version}; expected 3`);\n }\n throw new Error(`w7-frame: bad magic, expected NW7\\\\x03, got ${hex(magic)}`);\n }\n const beginEnvelope = await readEnvelope(buffer, \"batch-begin\");\n if (beginEnvelope.tag !== 1 /* BatchBegin */) {\n throw new Error(`w7-frame: first record must be batch-begin, got tag ${beginEnvelope.tag}`);\n }\n if (beginEnvelope.length > MAX_METADATA_BYTES) {\n throw new Error(\n `w7-frame: batch-begin length ${beginEnvelope.length} exceeds ${MAX_METADATA_BYTES}`\n );\n }\n const beginPayload = await buffer.readExact(beginEnvelope.length, \"batch-begin payload\");\n const begin = parseBatchBegin(beginPayload);\n const initialCheck = updateRecordCheck(CRC_SEED, beginEnvelope.header, beginPayload);\n handedOff = true;\n return {\n batchId: begin.id,\n mode: begin.mode,\n records: decodeRecords(stream, reader, buffer, options, initialCheck)\n };\n } catch (error) {\n if (!handedOff) await cancelReader(reader, error);\n throw error;\n }\n}\nasync function* decodeRecords(stream, reader, buffer, options, initialCheck) {\n const ownedPaths = new PathOwnership();\n const contentIds = /* @__PURE__ */ new Set();\n let active = null;\n let batchCheck = initialCheck;\n const summary = {\n recordCount: 1,\n pathCount: 0,\n deleteCount: 0,\n directoryCount: 0,\n fileCount: 0,\n chunkCount: 0,\n byteCount: 0\n };\n let completed = false;\n let failure = new DOMException(\"W7 consumer cancelled\", \"AbortError\");\n try {\n while (true) {\n throwIfAborted(options.signal);\n const envelope = await readEnvelope(buffer, \"record\");\n if (active && envelope.tag !== 5 /* FileChunk */ && envelope.tag !== 6 /* FileEnd */) {\n throw new Error(`w7-frame: file ${active.inode.path} ended without file-end`);\n }\n if (envelope.tag === 5 /* FileChunk */) {\n if (!active) throw new Error(\"w7-frame: file-chunk without active file\");\n const prefixLength = Math.min(envelope.length, 4 + MAX_CONTENT_ID_BYTES + 8);\n const idLengthBytes = await buffer.readExact(4, \"file-chunk content-id length\");\n const idLength = readU32LE(idLengthBytes, 0);\n if (idLength === 0 || idLength > MAX_CONTENT_ID_BYTES) {\n throw new Error(`w7-frame: invalid file-chunk content-id length ${idLength}`);\n }\n const remainingPrefixLength = idLength + 8;\n if (4 + remainingPrefixLength > prefixLength || 4 + remainingPrefixLength > envelope.length) {\n throw new Error(\"w7-frame: malformed file-chunk record length\");\n }\n const rest = await buffer.readExact(remainingPrefixLength, \"file-chunk header\");\n const contentId = decodeText(rest.subarray(0, idLength), \"file-chunk content id\");\n const chunkId = readU32LE(rest, idLength);\n const dataLength = readU32LE(rest, idLength + 4);\n if (envelope.length !== 4 + idLength + 8 + dataLength) {\n throw new Error(\"w7-frame: file-chunk payload length mismatch\");\n }\n if (contentId !== active.metadata.contentId) {\n throw new Error(`w7-frame: file-chunk content id ${contentId} does not own ${active.inode.path}`);\n }\n if (chunkId !== active.nextChunkId) {\n throw new Error(\n `w7-frame: ${active.inode.path}: expected chunk ${active.nextChunkId}, got ${chunkId}`\n );\n }\n if (chunkId >= active.inode.chunkCount) {\n throw new Error(`w7-frame: ${active.inode.path}: chunk ${chunkId} is out of range`);\n }\n const expectedBytes = Math.min(\n CHUNK_SIZE,\n active.inode.size - chunkId * CHUNK_SIZE\n );\n if (dataLength !== expectedBytes || dataLength > CHUNK_SIZE) {\n throw new Error(\n `w7-frame: ${active.inode.path}: chunk ${chunkId} has ${dataLength} bytes; expected ${expectedBytes}`\n );\n }\n const headerPayload = concatBytes(idLengthBytes, rest);\n let retention = null;\n try {\n retention = options.retainChunk ? await options.retainChunk(dataLength, options.signal) : noopRetention(dataLength);\n throwIfAborted(options.signal);\n const data = await buffer.readExact(dataLength, \"file-chunk data\");\n batchCheck = updateRecordCheck(batchCheck, envelope.header, headerPayload, data);\n active.check = crc32Update(active.check, data);\n active.nextChunkId++;\n active.receivedBytes += dataLength;\n summary.recordCount++;\n summary.chunkCount++;\n summary.byteCount += dataLength;\n const record = {\n type: \"file-chunk\",\n streamContentId: contentId,\n path: active.inode.path,\n chunkId,\n data,\n retention\n };\n retention = null;\n yield record;\n } finally {\n retention?.release();\n }\n continue;\n }\n if (envelope.length > MAX_METADATA_BYTES) {\n throw new Error(`w7-frame: metadata record length ${envelope.length} exceeds ${MAX_METADATA_BYTES}`);\n }\n const payload = await buffer.readExact(envelope.length, \"record payload\");\n if (envelope.tag !== 7 /* BatchEnd */) {\n batchCheck = updateRecordCheck(batchCheck, envelope.header, payload);\n summary.recordCount++;\n }\n switch (envelope.tag) {\n case 2 /* Delete */: {\n const metadata = parseDelete(payload);\n claimPath(ownedPaths, metadata.path);\n summary.pathCount++;\n summary.deleteCount++;\n yield { type: \"delete\", path: metadata.path };\n break;\n }\n case 3 /* Directory */: {\n const metadata = parseDirectory(payload);\n claimPath(ownedPaths, metadata.path);\n summary.pathCount++;\n summary.directoryCount++;\n yield { type: \"directory\", inode: directoryInode(metadata) };\n break;\n }\n case 4 /* FileBegin */: {\n const metadata = parseFileBegin(payload);\n claimPath(ownedPaths, metadata.path);\n if (contentIds.has(metadata.contentId)) {\n throw new Error(`w7-frame: duplicate stream content id ${metadata.contentId}`);\n }\n contentIds.add(metadata.contentId);\n const inode = fileInode(metadata);\n summary.pathCount++;\n summary.fileCount++;\n active = {\n metadata,\n inode,\n nextChunkId: 0,\n receivedBytes: 0,\n check: CRC_SEED\n };\n yield { type: \"file-begin\", streamContentId: metadata.contentId, inode };\n break;\n }\n case 6 /* FileEnd */: {\n if (!active) throw new Error(\"w7-frame: file-end without active file\");\n const metadata = parseFileEnd(payload);\n const actualCheck = crc32Finish(active.check);\n if (metadata.contentId !== active.metadata.contentId) {\n throw new Error(`w7-frame: file-end content id mismatch for ${active.inode.path}`);\n }\n if (metadata.size !== active.receivedBytes || metadata.size !== active.inode.size) {\n throw new Error(`w7-frame: file-end byte total mismatch for ${active.inode.path}`);\n }\n if (metadata.chunkCount !== active.nextChunkId || metadata.chunkCount !== active.inode.chunkCount) {\n throw new Error(`w7-frame: file-end chunk count mismatch for ${active.inode.path}`);\n }\n if (metadata.check !== actualCheck) {\n throw new Error(`w7-frame: file-end check mismatch for ${active.inode.path}`);\n }\n const record = {\n type: \"file-end\",\n streamContentId: metadata.contentId,\n path: active.inode.path,\n size: metadata.size,\n chunkCount: metadata.chunkCount,\n check: metadata.check\n };\n active = null;\n yield record;\n break;\n }\n case 7 /* BatchEnd */: {\n if (active) throw new Error(`w7-frame: batch-end while file ${active.inode.path} is active`);\n const actual = parseBatchEnd(payload);\n const expected = { ...summary, check: crc32Finish(batchCheck) };\n if (!sameSummary(actual, expected)) {\n throw new Error(\n `w7-frame: batch-end summary mismatch; expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`\n );\n }\n await buffer.ensureEof(stream);\n completed = true;\n yield { type: \"batch-end\", summary: actual };\n return;\n }\n case 1 /* BatchBegin */:\n throw new Error(\"w7-frame: duplicate batch-begin\");\n default:\n throw new Error(`w7-frame: unknown record tag ${envelope.tag}`);\n }\n }\n } catch (error) {\n failure = error;\n throw error;\n } finally {\n if (completed) {\n try {\n reader.releaseLock();\n } catch {\n }\n } else {\n await cancelReader(reader, failure);\n }\n }\n}\nfunction* encodeRecords(batchId, deletes, directories, files) {\n const state = {\n batchCheck: CRC_SEED,\n summary: {\n recordCount: 0,\n pathCount: 0,\n deleteCount: 0,\n directoryCount: 0,\n fileCount: 0,\n chunkCount: 0,\n byteCount: 0\n }\n };\n yield encodeMetadataRecord(1 /* BatchBegin */, { id: batchId, mode: MODE }, state);\n for (const path of deletes) {\n state.summary.pathCount++;\n state.summary.deleteCount++;\n yield encodeMetadataRecord(2 /* Delete */, { path }, state);\n }\n for (const inode of directories) {\n state.summary.pathCount++;\n state.summary.directoryCount++;\n yield encodeMetadataRecord(3 /* Directory */, {\n ...inodeMetadata(inode),\n kind: inode.kind\n }, state);\n }\n for (const file of files) {\n state.summary.pathCount++;\n state.summary.fileCount++;\n yield encodeMetadataRecord(4 /* FileBegin */, {\n ...inodeMetadata(file.inode),\n kind: file.inode.kind,\n contentId: file.contentId,\n size: file.inode.size,\n chunkCount: file.inode.chunkCount\n }, state);\n let fileCheck = CRC_SEED;\n for (const chunk of file.chunks) {\n const data = chunk.data;\n const contentBytes = new TextEncoder().encode(file.contentId);\n const prefix = new Uint8Array(4 + contentBytes.length + 8);\n writeU32LE(prefix, 0, contentBytes.length);\n prefix.set(contentBytes, 4);\n writeU32LE(prefix, 4 + contentBytes.length, chunk.chunkId);\n writeU32LE(prefix, 8 + contentBytes.length, data.byteLength);\n const header = recordHeader(5 /* FileChunk */, prefix.byteLength + data.byteLength);\n state.batchCheck = updateRecordCheck(state.batchCheck, header, prefix, data);\n state.summary.recordCount++;\n state.summary.chunkCount++;\n state.summary.byteCount += data.byteLength;\n fileCheck = crc32Update(fileCheck, data);\n yield [concatBytes(header, prefix), data];\n }\n yield encodeMetadataRecord(6 /* FileEnd */, {\n contentId: file.contentId,\n size: file.inode.size,\n chunkCount: file.inode.chunkCount,\n check: crc32Finish(fileCheck)\n }, state);\n }\n const end = {\n ...state.summary,\n check: crc32Finish(state.batchCheck)\n };\n yield encodeMetadataRecord(7 /* BatchEnd */, end);\n}\nfunction encodeMetadataRecord(tag, value, state) {\n const payload = new TextEncoder().encode(JSON.stringify(value));\n if (payload.byteLength > MAX_METADATA_BYTES) {\n throw new Error(`w7-frame: metadata record exceeds ${MAX_METADATA_BYTES} bytes`);\n }\n const header = recordHeader(tag, payload.byteLength);\n if (state && tag !== 7 /* BatchEnd */) {\n state.batchCheck = updateRecordCheck(state.batchCheck, header, payload);\n state.summary.recordCount++;\n }\n return [concatBytes(header, payload)];\n}\nfunction preparePayload(payload, batchId) {\n if (!payload || !Array.isArray(payload.inodes) || !Array.isArray(payload.chunks)) {\n throw new Error(\"w7-frame: payload must contain inode and chunk arrays\");\n }\n const ownedPaths = new PathOwnership();\n const deletes = [...payload.deletePaths ?? []];\n for (const path of deletes) claimPath(ownedPaths, canonicalPath(path, \"delete path\"));\n const chunksByPath = /* @__PURE__ */ new Map();\n for (const chunk of payload.chunks) {\n const path = canonicalPath(chunk.path, \"chunk path\");\n const list = chunksByPath.get(path);\n if (list) list.push(chunk);\n else chunksByPath.set(path, [chunk]);\n }\n const directories = [];\n const files = [];\n let fileIndex = 0;\n for (const inode of payload.inodes) {\n const path = canonicalPath(inode.path, \"inode path\");\n if (path !== inode.path) throw new Error(`w7-frame: noncanonical inode path ${inode.path}`);\n claimPath(ownedPaths, path);\n const normalizedInode = normalizeInode(inode);\n const fileChunks = chunksByPath.get(path) ?? [];\n if (normalizedInode.kind === \"directory\") {\n if (fileChunks.length > 0) throw new Error(`w7-frame: directory ${path} has chunks`);\n directories.push(normalizedInode);\n } else {\n validateChunks(normalizedInode, fileChunks);\n files.push({\n inode: normalizedInode,\n contentId: `${batchId}:${fileIndex++}`,\n chunks: fileChunks\n });\n }\n chunksByPath.delete(path);\n }\n if (chunksByPath.size > 0) {\n throw new Error(`w7-frame: chunk has no inode: ${chunksByPath.keys().next().value}`);\n }\n return { deletes, directories, files };\n}\nfunction parseBatchBegin(bytes) {\n const value = parseObject(bytes, \"batch-begin\", [\"id\", \"mode\"]);\n const id = boundedString(value.id, \"batch id\", MAX_BATCH_ID_BYTES);\n if (value.mode !== MODE) throw new Error(`w7-frame: unsupported batch mode ${String(value.mode)}`);\n return { id, mode: MODE };\n}\nfunction parseDelete(bytes) {\n const value = parseObject(bytes, \"delete\", [\"path\"]);\n return { path: canonicalPath(value.path, \"delete path\") };\n}\nfunction parseDirectory(bytes) {\n const value = parseObject(bytes, \"directory\", [\"path\", \"kind\", \"mtime\", \"mode\"], [\"atime\"]);\n if (value.kind !== \"directory\") {\n throw new Error(`w7-frame: unsupported directory kind ${String(value.kind)}`);\n }\n return { ...parseInodeMetadata(value, \"directory\"), kind: \"directory\" };\n}\nfunction parseFileBegin(bytes) {\n const value = parseObject(\n bytes,\n \"file-begin\",\n [\"path\", \"kind\", \"contentId\", \"size\", \"chunkCount\", \"mtime\", \"mode\"],\n [\"atime\"]\n );\n const base = parseInodeMetadata(value, \"file-begin\");\n if (value.kind !== \"file\" && value.kind !== \"symlink\") {\n throw new Error(`w7-frame: unsupported file-begin kind ${String(value.kind)}`);\n }\n const contentId = boundedString(value.contentId, \"stream content id\", MAX_CONTENT_ID_BYTES);\n if (!/^[A-Za-z0-9._:-]+$/.test(contentId)) {\n throw new Error(`w7-frame: invalid stream content id ${contentId}`);\n }\n const size = safeInteger(value.size, \"file size\");\n const chunkCount = u32(value.chunkCount, \"file chunk count\");\n const expected = size === 0 ? 0 : Math.ceil(size / CHUNK_SIZE);\n if (chunkCount !== expected) {\n throw new Error(`w7-frame: ${base.path}: expected ${expected} chunks, got ${chunkCount}`);\n }\n return { ...base, kind: value.kind, contentId, size, chunkCount };\n}\nfunction parseFileEnd(bytes) {\n const value = parseObject(bytes, \"file-end\", [\"contentId\", \"size\", \"chunkCount\", \"check\"]);\n return {\n contentId: boundedString(value.contentId, \"stream content id\", MAX_CONTENT_ID_BYTES),\n size: safeInteger(value.size, \"file-end size\"),\n chunkCount: u32(value.chunkCount, \"file-end chunk count\"),\n check: u32(value.check, \"file-end check\")\n };\n}\nfunction parseBatchEnd(bytes) {\n const keys = [\n \"recordCount\",\n \"pathCount\",\n \"deleteCount\",\n \"directoryCount\",\n \"fileCount\",\n \"chunkCount\",\n \"byteCount\",\n \"check\"\n ];\n const value = parseObject(bytes, \"batch-end\", keys);\n return {\n recordCount: safeInteger(value.recordCount, \"batch record count\"),\n pathCount: safeInteger(value.pathCount, \"batch path count\"),\n deleteCount: safeInteger(value.deleteCount, \"batch delete count\"),\n directoryCount: safeInteger(value.directoryCount, \"batch directory count\"),\n fileCount: safeInteger(value.fileCount, \"batch file count\"),\n chunkCount: safeInteger(value.chunkCount, \"batch chunk count\"),\n byteCount: safeInteger(value.byteCount, \"batch byte count\"),\n check: u32(value.check, \"batch check\")\n };\n}\nfunction parseInodeMetadata(value, label) {\n return {\n path: canonicalPath(value.path, `${label} path`),\n ...value.atime === void 0 ? {} : { atime: safeInteger(value.atime, `${label} atime`) },\n mtime: safeInteger(value.mtime, `${label} mtime`),\n mode: u32(value.mode, `${label} mode`)\n };\n}\nfunction parseObject(bytes, label, required, optional = []) {\n let value;\n try {\n value = JSON.parse(decodeText(bytes, label));\n } catch (error) {\n if (error instanceof Error && error.message.startsWith(\"w7-frame:\")) throw error;\n throw new Error(`w7-frame: invalid ${label} JSON: ${errorMessage(error)}`);\n }\n if (!isObject(value) || Array.isArray(value)) throw new Error(`w7-frame: ${label} must be an object`);\n const allowed = /* @__PURE__ */ new Set([...required, ...optional]);\n for (const key of required) {\n if (!Object.hasOwn(value, key)) throw new Error(`w7-frame: ${label} missing ${key}`);\n }\n for (const key of Object.keys(value)) {\n if (!allowed.has(key)) throw new Error(`w7-frame: ${label} has unknown field ${key}`);\n }\n return value;\n}\nfunction normalizeInode(inode) {\n canonicalPath(inode.path, \"inode path\");\n if (inode.parentPath !== parentPath(inode.path)) {\n throw new Error(`w7-frame: ${inode.path}: noncanonical parent path ${inode.parentPath}`);\n }\n safeInteger(inode.size, `${inode.path} size`);\n u32(inode.chunkCount, `${inode.path} chunk count`);\n safeInteger(inode.mtime, `${inode.path} mtime`);\n if (inode.atime !== void 0) safeInteger(inode.atime, `${inode.path} atime`);\n u32(inode.mode, `${inode.path} mode`);\n const rawKind = inode.kind ?? (inode.isDir ? \"directory\" : \"file\");\n if (rawKind !== \"file\" && rawKind !== \"directory\" && rawKind !== \"symlink\") {\n throw new Error(`w7-frame: unsupported inode kind ${String(rawKind)}`);\n }\n const kind = rawKind;\n if (kind === \"directory\" && !inode.isDir) {\n throw new Error(`w7-frame: directory inode ${inode.path} must be a directory`);\n }\n if (kind !== \"directory\" && inode.isDir) {\n throw new Error(`w7-frame: ${kind} inode ${inode.path} cannot be a directory`);\n }\n if (kind === \"directory\" && (inode.size !== 0 || inode.chunkCount !== 0)) {\n throw new Error(`w7-frame: directory ${inode.path} must have zero size and chunks`);\n }\n if (kind !== \"directory\") {\n const expected = inode.size === 0 ? 0 : Math.ceil(inode.size / CHUNK_SIZE);\n if (inode.chunkCount !== expected) {\n throw new Error(`w7-frame: ${inode.path}: expected ${expected} chunks, got ${inode.chunkCount}`);\n }\n return { ...inode, kind, isDir: false };\n }\n return { ...inode, kind, isDir: true };\n}\nfunction validateChunks(inode, chunks) {\n if (chunks.length !== inode.chunkCount) {\n throw new Error(`w7-frame: ${inode.path}: expected ${inode.chunkCount} chunks, got ${chunks.length}`);\n }\n chunks.sort((left, right) => left.chunkId - right.chunkId);\n for (let index = 0; index < chunks.length; index++) {\n const chunk = chunks[index];\n if (chunk.chunkId !== index) {\n throw new Error(`w7-frame: ${inode.path}: expected chunk ${index}, got ${chunk.chunkId}`);\n }\n const expected = Math.min(CHUNK_SIZE, inode.size - index * CHUNK_SIZE);\n if (!(chunk.data instanceof Uint8Array) || chunk.data.byteLength !== expected) {\n throw new Error(`w7-frame: ${inode.path}: chunk ${index} must contain ${expected} bytes`);\n }\n }\n}\nfunction directoryInode(metadata) {\n return {\n path: metadata.path,\n parentPath: parentPath(metadata.path),\n kind: metadata.kind,\n isDir: true,\n size: 0,\n atime: metadata.atime,\n mtime: metadata.mtime,\n mode: metadata.mode,\n chunkCount: 0\n };\n}\nfunction fileInode(metadata) {\n return {\n path: metadata.path,\n parentPath: parentPath(metadata.path),\n kind: metadata.kind,\n isDir: false,\n size: metadata.size,\n atime: metadata.atime,\n mtime: metadata.mtime,\n mode: metadata.mode,\n chunkCount: metadata.chunkCount\n };\n}\nfunction inodeMetadata(inode) {\n return {\n path: inode.path,\n ...inode.atime === void 0 ? {} : { atime: inode.atime },\n mtime: inode.mtime,\n mode: inode.mode\n };\n}\nfunction canonicalPath(value, label) {\n const path = boundedString(value, label, MAX_PATH_BYTES);\n if (path.includes(\"\\0\")) throw new Error(`w7-frame: ${label} contains NUL`);\n const normalized = normalizePath(path);\n if (!path || normalized !== path) throw new Error(`w7-frame: noncanonical ${label}: ${path}`);\n return path;\n}\nvar PathOwnership = class {\n paths = /* @__PURE__ */ new Set();\n pathBytes = 0;\n claim(path) {\n if (this.paths.has(path)) throw new Error(`w7-frame: duplicate path ownership: ${path}`);\n if (this.paths.size >= W7_MAX_PATHS_PER_BATCH) {\n throw new Error(`w7-frame: batch exceeds ${W7_MAX_PATHS_PER_BATCH} owned paths`);\n }\n const nextPathBytes = this.pathBytes + new TextEncoder().encode(path).byteLength;\n if (nextPathBytes > W7_MAX_OWNED_PATH_BYTES) {\n throw new Error(\n `w7-frame: owned path bytes exceed ${W7_MAX_OWNED_PATH_BYTES}`\n );\n }\n this.paths.add(path);\n this.pathBytes = nextPathBytes;\n }\n};\nfunction claimPath(paths, path) {\n paths.claim(path);\n}\nfunction normalizePath(path) {\n const out = [];\n for (const segment of path.split(\"/\")) {\n if (segment === \"..\") {\n if (out.length > 0) out.pop();\n } else if (segment !== \"\" && segment !== \".\") {\n out.push(segment);\n }\n }\n return out.join(\"/\");\n}\nfunction parentPath(path) {\n const index = path.lastIndexOf(\"/\");\n return index < 0 ? \"\" : path.slice(0, index);\n}\nfunction boundedString(value, label, maxBytes) {\n if (typeof value !== \"string\" || value.length === 0) {\n throw new Error(`w7-frame: ${label} must be a non-empty string`);\n }\n const length = new TextEncoder().encode(value).byteLength;\n if (length > maxBytes) throw new Error(`w7-frame: ${label} exceeds ${maxBytes} bytes`);\n return value;\n}\nfunction safeInteger(value, label) {\n if (!Number.isSafeInteger(value) || value < 0) {\n throw new Error(`w7-frame: ${label} must be a non-negative safe integer`);\n }\n return value;\n}\nfunction u32(value, label) {\n const integer = safeInteger(value, label);\n if (integer > 4294967295) throw new Error(`w7-frame: ${label} exceeds uint32`);\n return integer;\n}\nfunction sameSummary(left, right) {\n return left.recordCount === right.recordCount && left.pathCount === right.pathCount && left.deleteCount === right.deleteCount && left.directoryCount === right.directoryCount && left.fileCount === right.fileCount && left.chunkCount === right.chunkCount && left.byteCount === right.byteCount && left.check === right.check;\n}\nasync function readEnvelope(buffer, label) {\n const header = await buffer.readExact(5, `${label} header`);\n return { tag: header[0], length: readU32LE(header, 1), header };\n}\nfunction recordHeader(tag, length) {\n const header = new Uint8Array(5);\n header[0] = tag;\n writeU32LE(header, 1, length);\n return header;\n}\nfunction writeU32LE(out, offset, value) {\n out[offset] = value & 255;\n out[offset + 1] = value >>> 8 & 255;\n out[offset + 2] = value >>> 16 & 255;\n out[offset + 3] = value >>> 24 & 255;\n}\nfunction readU32LE(bytes, offset) {\n return (bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24) >>> 0;\n}\nfunction updateRecordCheck(seed, ...parts) {\n let check = seed;\n for (const part of parts) check = crc32Update(check, part);\n return check;\n}\nvar CRC_SEED = 4294967295;\nvar crcTable = null;\nfunction crc32Update(check, bytes) {\n const table = crcTable ??= createCrcTable();\n let value = check;\n for (const byte of bytes) value = table[(value ^ byte) & 255] ^ value >>> 8;\n return value >>> 0;\n}\nfunction crc32Finish(check) {\n return (check ^ 4294967295) >>> 0;\n}\nfunction createCrcTable() {\n const table = new Uint32Array(256);\n for (let index = 0; index < table.length; index++) {\n let value = index;\n for (let bit = 0; bit < 8; bit++) {\n value = (value & 1) !== 0 ? 3988292384 ^ value >>> 1 : value >>> 1;\n }\n table[index] = value >>> 0;\n }\n return table;\n}\nfunction noopRetention(bytes) {\n return { bytes, release() {\n } };\n}\nfunction decodeText(bytes, label) {\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes);\n } catch (error) {\n throw new Error(`w7-frame: invalid UTF-8 in ${label}: ${errorMessage(error)}`);\n }\n}\nfunction concatBytes(...parts) {\n const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));\n let offset = 0;\n for (const part of parts) {\n output.set(part, offset);\n offset += part.byteLength;\n }\n return output;\n}\nfunction bytesEqual(left, right) {\n return left.length === right.length && left.every((byte, index) => byte === right[index]);\n}\nfunction hex(bytes) {\n return Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\" \");\n}\nfunction isObject(value) {\n return typeof value === \"object\" && value !== null;\n}\nfunction errorMessage(error) {\n return error instanceof Error ? error.message : String(error);\n}\nfunction throwIfAborted(signal) {\n if (!signal?.aborted) return;\n throw new DOMException(\n signal.reason instanceof Error ? signal.reason.message : String(signal.reason ?? \"Aborted\"),\n \"AbortError\"\n );\n}\nasync function cancelReader(reader, reason) {\n try {\n await reader.cancel(reason);\n } catch {\n }\n try {\n reader.releaseLock();\n } catch {\n }\n}\nvar ExactByteReader = class {\n constructor(reader) {\n this.reader = reader;\n }\n reader;\n done = false;\n async readExact(length, label) {\n if (length === 0) return new Uint8Array(0);\n const output = new Uint8Array(length);\n let offset = 0;\n while (offset < length) {\n if (this.done) {\n throw new Error(\n `w7-frame: stream ended ${offset} bytes into expected ${length}-byte ${label}`\n );\n }\n const next = await this.reader.read(new Uint8Array(length - offset));\n if (next.done) this.done = true;\n else if (next.value.byteLength > 0) {\n output.set(next.value, offset);\n offset += next.value.byteLength;\n }\n }\n return output;\n }\n async ensureEof(stream) {\n if (this.done) return;\n this.reader.releaseLock();\n const reader = stream.getReader();\n try {\n const next = await reader.read();\n if (!next.done && next.value.byteLength > 0) {\n await reader.cancel(new Error(\"w7-frame: trailing bytes after batch-end\"));\n throw new Error(\"w7-frame: trailing bytes after batch-end\");\n }\n this.done = true;\n } finally {\n try {\n reader.releaseLock();\n } catch {\n }\n }\n }\n};";
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The workerd {@link FacetHost}: a facet is a dynamic worker.
|
|
3
3
|
*
|
|
4
|
-
* The whole adapter is the option
|
|
4
|
+
* The whole adapter is the option renames below, because `NimbusLoaderPool`
|
|
5
5
|
* already IS the port's shape — `submit` and `dispose`, with the same meanings.
|
|
6
6
|
* The one thing it spells differently is the supervisor capability, which it
|
|
7
7
|
* takes as a pid plus a separate flag saying whether to bind one at all; the
|
|
8
8
|
* port collapses the pair, since a facet with the binding and no pid can read
|
|
9
|
-
* the session and never write to it.
|
|
9
|
+
* the session and never write to it. `reuse` is the pool's `cacheScope` under
|
|
10
|
+
* the name the port gives it: who a warm facet may answer for.
|
|
10
11
|
*/
|
|
11
12
|
import type { FacetHost } from '@nimbus-sh/core/runtime/facet-host.js';
|
|
12
13
|
import type { FacetManager } from '../facets/manager.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facet-loader-host.d.ts","sourceRoot":"","sources":["../../src/runtime/facet-loader-host.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"facet-loader-host.d.ts","sourceRoot":"","sources":["../../src/runtime/facet-loader-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAIV,SAAS,EAEV,MAAM,uCAAuC,CAAC;AAG/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAGzD,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,kBAAkB,GAAG,SAAS,CA6BhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,YAAY,GACrB;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,kBAAkB,CAAA;CAAE,CAO3C;AAED,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,SAAS,CAGrE"}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* - irb history pickling.
|
|
26
26
|
*/
|
|
27
27
|
import { ReplSession } from './repl-session.js';
|
|
28
|
-
import { buildRubyPreamble } from '
|
|
28
|
+
import { buildRubyPreamble } from '@nimbus-sh/core/runtime/ruby-runner.js';
|
|
29
29
|
import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
30
30
|
class RubyReplAdapter {
|
|
31
31
|
pool = null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ruby-resident.ts — a Ruby program that keeps serving, as a Durable Object
|
|
3
|
+
* facet.
|
|
4
|
+
*
|
|
5
|
+
* The half of the Ruby runtime that names workerd. Everything about running
|
|
6
|
+
* the interpreter is in `@nimbus-sh/core`; this is the substrate a program
|
|
7
|
+
* that outlives its invocation needs — an actor that stays alive between
|
|
8
|
+
* requests, with inbound HTTP routed into the socket the program bound. A
|
|
9
|
+
* host without one supplies no {@link RubyResidentStart}, and such a program
|
|
10
|
+
* is refused rather than quietly run as a one-shot that dies with the call.
|
|
11
|
+
*/
|
|
12
|
+
import { type RubyResidentStart } from '@nimbus-sh/core/runtime/ruby-runner.js';
|
|
13
|
+
import type { FacetManager } from '../facets/manager.js';
|
|
14
|
+
/**
|
|
15
|
+
* Start a Ruby program that binds a port as its own process, so it outlives
|
|
16
|
+
* the invocation that started it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function rubyResidentStart(facetMgr: FacetManager): RubyResidentStart;
|
|
19
|
+
export declare function buildRubySocketProcessWorker(preamble: string): string;
|
|
20
|
+
//# sourceMappingURL=ruby-resident.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ruby-resident.d.ts","sourceRoot":"","sources":["../../src/runtime/ruby-resident.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,wCAAwC,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAezD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY,GAAG,iBAAiB,CA8E3E;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiLrE"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ruby-resident.ts — a Ruby program that keeps serving, as a Durable Object
|
|
3
|
+
* facet.
|
|
4
|
+
*
|
|
5
|
+
* The half of the Ruby runtime that names workerd. Everything about running
|
|
6
|
+
* the interpreter is in `@nimbus-sh/core`; this is the substrate a program
|
|
7
|
+
* that outlives its invocation needs — an actor that stays alive between
|
|
8
|
+
* requests, with inbound HTTP routed into the socket the program bound. A
|
|
9
|
+
* host without one supplies no {@link RubyResidentStart}, and such a program
|
|
10
|
+
* is refused rather than quietly run as a one-shot that dies with the call.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { buildRubyPreamble, normalizeRubyFacetResult, } from '@nimbus-sh/core/runtime/ruby-runner.js';
|
|
14
|
+
import { VIRTUAL_SOCKET_KERNEL_SRC } from '@nimbus-sh/core/runtime/virtual-socket-kernel.generated.js';
|
|
15
|
+
const RubySocketProcessBootResponseSchema = z.object({
|
|
16
|
+
state: z.string().optional(),
|
|
17
|
+
port: z.number().optional(),
|
|
18
|
+
stdout: z.string().optional(),
|
|
19
|
+
stderr: z.string().optional(),
|
|
20
|
+
result: z.unknown().optional(),
|
|
21
|
+
}).passthrough();
|
|
22
|
+
/**
|
|
23
|
+
* Start a Ruby program that binds a port as its own process, so it outlives
|
|
24
|
+
* the invocation that started it.
|
|
25
|
+
*/
|
|
26
|
+
export function rubyResidentStart(facetMgr) {
|
|
27
|
+
return async function startResidentRuby(args) {
|
|
28
|
+
const command = args.command;
|
|
29
|
+
const workerCode = buildRubySocketProcessWorker(buildRubyPreamble());
|
|
30
|
+
const spawned = await facetMgr.spawnWorker(workerCode, command, args.cwd, {
|
|
31
|
+
compatibilityFlags: ['nodejs_compat'],
|
|
32
|
+
// By path, not by value: the image is 34.3 MiB — more than a single RPC
|
|
33
|
+
// value may carry — so whichever host runs this process reads it itself.
|
|
34
|
+
vfsWasmModules: { 'ruby+stdlib.wasm': args.wasmVfsPath },
|
|
35
|
+
startArgs: args.startArgs,
|
|
36
|
+
}).catch(() => null);
|
|
37
|
+
if (!spawned) {
|
|
38
|
+
return { exitCode: 1, stdout: '', stderr: 'ruby process boot failed\n' };
|
|
39
|
+
}
|
|
40
|
+
const parsed = RubySocketProcessBootResponseSchema.safeParse(spawned.boot);
|
|
41
|
+
if (!parsed.success) {
|
|
42
|
+
facetMgr.finishProcess(spawned.pid, 1, 'ruby process boot failed');
|
|
43
|
+
return {
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
stdout: '',
|
|
46
|
+
stderr: 'ruby process boot failed\n',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const boot = parsed.data;
|
|
50
|
+
if (boot.state === 'listening' && typeof boot.port === 'number' && boot.port > 0) {
|
|
51
|
+
facetMgr.registerPort(spawned.pid, Number(boot.port));
|
|
52
|
+
const routeablePorts = await facetMgr.waitForRouteablePorts(spawned.pid);
|
|
53
|
+
const routeablePort = routeablePorts.includes(Number(boot.port)) ? Number(boot.port) : routeablePorts[0];
|
|
54
|
+
if (!routeablePort) {
|
|
55
|
+
facetMgr.kill(spawned.pid);
|
|
56
|
+
return {
|
|
57
|
+
exitCode: 1,
|
|
58
|
+
stdout: boot.stdout || '',
|
|
59
|
+
stderr: `${boot.stderr || ''}ruby: virtual socket port ${boot.port} failed to attach a route handler\n`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
exitCode: 0,
|
|
64
|
+
stdout: `${boot.stdout || ''}\x1b[2m[started (long-running): pid=${spawned.pid} cmd="${command}" port=${routeablePort}]\x1b[0m\n`,
|
|
65
|
+
stderr: boot.stderr || '',
|
|
66
|
+
spawnedPid: spawned.pid,
|
|
67
|
+
port: routeablePort,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const reservedPorts = await facetMgr.waitForRouteablePorts(spawned.pid);
|
|
71
|
+
if (reservedPorts.length > 0) {
|
|
72
|
+
return {
|
|
73
|
+
exitCode: 0,
|
|
74
|
+
stdout: `${boot.stdout || ''}\x1b[2m[started (long-running): pid=${spawned.pid} cmd="${command}" port=${reservedPorts[0]}]\x1b[0m\n`,
|
|
75
|
+
stderr: boot.stderr || '',
|
|
76
|
+
spawnedPid: spawned.pid,
|
|
77
|
+
port: reservedPorts[0],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (boot.state === 'exited') {
|
|
81
|
+
const result = normalizeRubyFacetResult(boot.result) || {
|
|
82
|
+
exitCode: 1,
|
|
83
|
+
stdout: '',
|
|
84
|
+
stderr: 'ruby process returned an invalid exit payload\n',
|
|
85
|
+
};
|
|
86
|
+
facetMgr.finishProcess(spawned.pid, result.exitCode, result.stderr || 'ruby process exited');
|
|
87
|
+
return {
|
|
88
|
+
exitCode: result.exitCode,
|
|
89
|
+
stdout: result.stdout,
|
|
90
|
+
stderr: result.stderr || result.error || '',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
exitCode: 0,
|
|
95
|
+
stdout: `${boot.stdout || ''}\x1b[2m[started (long-running): pid=${spawned.pid} cmd="${command}"]\x1b[0m\n`,
|
|
96
|
+
stderr: boot.stderr || '',
|
|
97
|
+
spawnedPid: spawned.pid,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function buildRubySocketProcessWorker(preamble) {
|
|
102
|
+
return [
|
|
103
|
+
'import { DurableObject } from "cloudflare:workers";',
|
|
104
|
+
"import __NIMBUS_WASM_ruby_stdlib from './ruby+stdlib.wasm';",
|
|
105
|
+
'globalThis.__NIMBUS_WASM = globalThis.__NIMBUS_WASM || {};',
|
|
106
|
+
"globalThis.__NIMBUS_WASM['ruby+stdlib.wasm'] = __NIMBUS_WASM_ruby_stdlib;",
|
|
107
|
+
'',
|
|
108
|
+
VIRTUAL_SOCKET_KERNEL_SRC,
|
|
109
|
+
'',
|
|
110
|
+
// Listener lifecycle only. Socket bytes never cross this bridge: Ruby opens
|
|
111
|
+
// both accepted and dialed connections as file descriptors and reads and
|
|
112
|
+
// writes them as ordinary IO.
|
|
113
|
+
'globalThis.__nimbusRubySockets = {',
|
|
114
|
+
' listen(port) { return globalThis.__nimbusVirtualSockets.listen(Number(port)); },',
|
|
115
|
+
' closeListener(port) { globalThis.__nimbusVirtualSockets.closeListener(Number(port)); return true; },',
|
|
116
|
+
' pending(port) { return globalThis.__nimbusVirtualSockets.pending(Number(port)); },',
|
|
117
|
+
' acceptNowJson(port) { const conn = globalThis.__nimbusVirtualSockets.acceptNow(Number(port)); return conn ? JSON.stringify(conn) : ""; },',
|
|
118
|
+
'};',
|
|
119
|
+
'',
|
|
120
|
+
// Outbound half of the same loopback the shell's curl and node's patched
|
|
121
|
+
// fetch use: one mechanism, reached here through the supervisor RPC.
|
|
122
|
+
'globalThis.__nimbusVirtualSocketRouteLoopback = function __nimbusVirtualSocketRouteLoopback(port, request) {',
|
|
123
|
+
' const supervisor = globalThis.__nimbusRubySupervisor;',
|
|
124
|
+
' if (!supervisor || typeof supervisor.routeLoopback !== "function") {',
|
|
125
|
+
' return Promise.reject(new Error("this Ruby process has no supervisor binding for loopback routing"));',
|
|
126
|
+
' }',
|
|
127
|
+
' return Promise.resolve(supervisor.routeLoopback(Number(port), request));',
|
|
128
|
+
'};',
|
|
129
|
+
'',
|
|
130
|
+
'globalThis.__nimbusVirtualPortRegistrationPromises = globalThis.__nimbusVirtualPortRegistrationPromises || [];',
|
|
131
|
+
'globalThis.__nimbusVirtualSocketDidListen = function __nimbusVirtualSocketDidListen(port) {',
|
|
132
|
+
' const supervisor = globalThis.__nimbusRubySupervisor;',
|
|
133
|
+
' if (!supervisor || typeof supervisor.registerPort !== "function") return;',
|
|
134
|
+
' try {',
|
|
135
|
+
' const p = supervisor.registerPort(Number(port)).catch((e) => {',
|
|
136
|
+
' const msg = e && e.message ? e.message : String(e);',
|
|
137
|
+
' (globalThis.__nimbusRubyStderr || (globalThis.__nimbusRubyStderr = [])).push("[ruby-runner] port registration failed: " + msg + "\\n");',
|
|
138
|
+
' });',
|
|
139
|
+
' globalThis.__nimbusVirtualPortRegistrationPromises.push(p);',
|
|
140
|
+
' } catch (e) {',
|
|
141
|
+
' const msg = e && e.message ? e.message : String(e);',
|
|
142
|
+
' (globalThis.__nimbusRubyStderr || (globalThis.__nimbusRubyStderr = [])).push("[ruby-runner] port registration failed: " + msg + "\\n");',
|
|
143
|
+
' }',
|
|
144
|
+
'};',
|
|
145
|
+
'',
|
|
146
|
+
preamble,
|
|
147
|
+
'',
|
|
148
|
+
// Drive the process when a connection is queued. This is the whole of the
|
|
149
|
+
// runtime's involvement in serving: it knows nothing about what is
|
|
150
|
+
// listening, only that the process should run until it parks.
|
|
151
|
+
//
|
|
152
|
+
// Steps go through the shared resume queue in the preamble, so a handler
|
|
153
|
+
// that parks does not stall other connections and no two drivers can enter
|
|
154
|
+
// a live fiber at once.
|
|
155
|
+
// Timed work the process still owes: the wall-clock moment its earliest
|
|
156
|
+
// sleeper is due. It lives on the global rather than in one driver's
|
|
157
|
+
// closure because no single request may own it. A timer belongs to the
|
|
158
|
+
// request context that created it, and workerd cancels that timer without
|
|
159
|
+
// a word when the request ends - so a driver anchored to the first
|
|
160
|
+
// connection stops dead the moment that connection answers, and every
|
|
161
|
+
// other connection waits out the response timeout instead.
|
|
162
|
+
'globalThis.__nimbusRubyWakeAt = globalThis.__nimbusRubyWakeAt || null;',
|
|
163
|
+
'globalThis.__nimbusRubyIdleDrivers = globalThis.__nimbusRubyIdleDrivers || new Set();',
|
|
164
|
+
'function __nimbusRubyNoteWake(wakeAfter) {',
|
|
165
|
+
' globalThis.__nimbusRubyWakeAt = (wakeAfter === null || wakeAfter === undefined)',
|
|
166
|
+
' ? null',
|
|
167
|
+
' : Date.now() + Math.max(0, wakeAfter) * 1000;',
|
|
168
|
+
' if (globalThis.__nimbusRubyWakeAt === null) return;',
|
|
169
|
+
' const waiting = Array.from(globalThis.__nimbusRubyIdleDrivers);',
|
|
170
|
+
' globalThis.__nimbusRubyIdleDrivers.clear();',
|
|
171
|
+
' for (const wake of waiting) wake();',
|
|
172
|
+
'}',
|
|
173
|
+
// So instead every live request drives, for as long as it is live, and the
|
|
174
|
+
// moment one of them answers the others are already carrying the work.
|
|
175
|
+
// Steps are serialized by the resume queue, and a driver that wakes to
|
|
176
|
+
// find the deadline already moved knows another one got there first.
|
|
177
|
+
'async function __nimbusRubyDrive() {',
|
|
178
|
+
' for (;;) {',
|
|
179
|
+
' const due = globalThis.__nimbusRubyWakeAt;',
|
|
180
|
+
' if (due === null) {',
|
|
181
|
+
' // Nothing is due yet. Stay available anyway: this request holds a',
|
|
182
|
+
' // live context, and whichever request discovers the next piece of',
|
|
183
|
+
' // timed work may answer and be gone before that work comes due.',
|
|
184
|
+
' await new Promise((resolve) => globalThis.__nimbusRubyIdleDrivers.add(resolve));',
|
|
185
|
+
' continue;',
|
|
186
|
+
' }',
|
|
187
|
+
' const delay = due - Date.now();',
|
|
188
|
+
' if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));',
|
|
189
|
+
' if (globalThis.__nimbusRubyWakeAt !== due) continue;',
|
|
190
|
+
' const step = await globalThis.__nimbusRubyStep();',
|
|
191
|
+
' if (!step.resumed || !step.alive) { globalThis.__nimbusRubyWakeAt = null; return; }',
|
|
192
|
+
' __nimbusRubyNoteWake(step.wakeAfter);',
|
|
193
|
+
' }',
|
|
194
|
+
'}',
|
|
195
|
+
'globalThis.__nimbusVirtualSocketRequestQueued = async function __nimbusVirtualSocketRequestQueued(port) {',
|
|
196
|
+
' const step = await globalThis.__nimbusRubyStep();',
|
|
197
|
+
' if (!step.resumed) return false;',
|
|
198
|
+
' __nimbusRubyNoteWake(step.wakeAfter);',
|
|
199
|
+
' __nimbusRubyDrive().catch((e) => {',
|
|
200
|
+
' (globalThis.__nimbusRubyStderr || (globalThis.__nimbusRubyStderr = [])).push(',
|
|
201
|
+
' "[ruby-runner] driving the process failed: " + ((e && e.message) || e) + "\\n");',
|
|
202
|
+
' });',
|
|
203
|
+
' return true;',
|
|
204
|
+
'};',
|
|
205
|
+
'',
|
|
206
|
+
'async function __nimbusStartRubyProcess(args) {',
|
|
207
|
+
' if (!globalThis.__nimbusRubyProcessPromise) {',
|
|
208
|
+
' const stdoutStart = (globalThis.__nimbusRubyStdout || []).length;',
|
|
209
|
+
' const stderrStart = (globalThis.__nimbusRubyStderr || []).length;',
|
|
210
|
+
' globalThis.__nimbusRubyProcessOutputStart = { stdoutStart, stderrStart };',
|
|
211
|
+
' globalThis.__nimbusRubyProcessArgs = {',
|
|
212
|
+
' userCode: args.userCode,',
|
|
213
|
+
' rbArgv: args.rbArgv || [],',
|
|
214
|
+
' userEnv: args.userEnv || {},',
|
|
215
|
+
' progName: args.progName || "ruby",',
|
|
216
|
+
' cwd: args.cwd || "/home/user",',
|
|
217
|
+
' fsSnapshot: args.fsSnapshot,',
|
|
218
|
+
' };',
|
|
219
|
+
' globalThis.__nimbusRubyProcessPromise = globalThis.__rubyRun(globalThis.__nimbusRubyProcessArgs).then((result) => {',
|
|
220
|
+
' globalThis.__nimbusRubyProcessResult = result;',
|
|
221
|
+
' return result;',
|
|
222
|
+
' });',
|
|
223
|
+
' }',
|
|
224
|
+
' const started = globalThis.__nimbusRubyProcessOutputStart || { stdoutStart: 0, stderrStart: 0 };',
|
|
225
|
+
' const listen = globalThis.__nimbusVirtualSockets.waitForListen(10_000).then((port) => ({ state: port ? "listening" : "pending", port }));',
|
|
226
|
+
' const exit = globalThis.__nimbusRubyProcessPromise.then((result) => ({ state: "exited", result }));',
|
|
227
|
+
' const first = await Promise.race([listen, exit]);',
|
|
228
|
+
' const registrations = globalThis.__nimbusVirtualPortRegistrationPromises || [];',
|
|
229
|
+
' if (registrations.length > 0) await Promise.allSettled(registrations.splice(0));',
|
|
230
|
+
' const stdout = (globalThis.__nimbusRubyStdout || []).slice(started.stdoutStart).join("");',
|
|
231
|
+
' const stderr = (globalThis.__nimbusRubyStderr || []).slice(started.stderrStart).join("");',
|
|
232
|
+
' if (first.state === "listening") return { state: "listening", port: first.port, stdout, stderr };',
|
|
233
|
+
' if (first.state === "exited") return { state: "exited", result: first.result, stdout, stderr };',
|
|
234
|
+
' const currentPort = globalThis.__nimbusVirtualSockets.firstListeningPort();',
|
|
235
|
+
' if (currentPort) return { state: "listening", port: currentPort, stdout, stderr };',
|
|
236
|
+
' return { state: "running", stdout, stderr };',
|
|
237
|
+
'}',
|
|
238
|
+
'',
|
|
239
|
+
// Only adopt a real binding: routed handleHttpRequest/fetch hops resolve
|
|
240
|
+
// the entrypoint without a supervisor, and overwriting with undefined
|
|
241
|
+
// would drop the live stub the process needs for its whole lifetime.
|
|
242
|
+
'function __nimbusAdoptRubySupervisor(env) {',
|
|
243
|
+
' const supervisor = env && env.SUPERVISOR;',
|
|
244
|
+
' if (supervisor) globalThis.__nimbusRubySupervisor = supervisor;',
|
|
245
|
+
// The WASI filesystem takes the same stub. That is what turns the
|
|
246
|
+
// spawn-time seed into a cache over the session VFS, so a server that
|
|
247
|
+
// never exits still persists its writes instead of losing them entirely.
|
|
248
|
+
' __wasiAdoptSupervisor(supervisor);',
|
|
249
|
+
'}',
|
|
250
|
+
// A resident process parks between requests, and parking is the only
|
|
251
|
+
// moment "durable while running" can be made true: by the time the caller
|
|
252
|
+
// holds a response, everything the request wrote has reached the VFS.
|
|
253
|
+
'async function __nimbusParkRuby(value) {',
|
|
254
|
+
// Revalidate drains first, then spends ONE round trip on the subtree
|
|
255
|
+
// revision. An unchanged subtree keeps the whole cache; a changed one
|
|
256
|
+
// drops the clean half so the next read sees another process's writes.
|
|
257
|
+
' await __wasiRevalidateFS();',
|
|
258
|
+
' return value;',
|
|
259
|
+
'}',
|
|
260
|
+
'export class NimbusProcess extends DurableObject {',
|
|
261
|
+
' async startProcess(args) {',
|
|
262
|
+
' __nimbusAdoptRubySupervisor(this.env);',
|
|
263
|
+
' return __nimbusParkRuby(await __nimbusStartRubyProcess(args || {}));',
|
|
264
|
+
' }',
|
|
265
|
+
' async fetch(request) {',
|
|
266
|
+
' __nimbusAdoptRubySupervisor(this.env);',
|
|
267
|
+
' return this.handleHttpRequest(request);',
|
|
268
|
+
' }',
|
|
269
|
+
' async handleHttpRequest(request) {',
|
|
270
|
+
' __nimbusAdoptRubySupervisor(this.env);',
|
|
271
|
+
' const hinted = Number(request.headers.get("X-Nimbus-Port") || 0);',
|
|
272
|
+
' const port = hinted || Array.from(globalThis.__nimbusVirtualSockets.listeners.keys())[0];',
|
|
273
|
+
' if (!port) return new Response("Nimbus Ruby process has no listening virtual socket", { status: 502 });',
|
|
274
|
+
' return __nimbusParkRuby(await globalThis.__nimbusVirtualSockets.handleHttpRequest(port, request));',
|
|
275
|
+
' }',
|
|
276
|
+
'}',
|
|
277
|
+
].join('\n');
|
|
278
|
+
}
|
package/dist/session/init.d.ts
CHANGED
|
@@ -3,12 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Why this is one big function and not a class:
|
|
5
5
|
* initSession runs once per /ws upgrade and walks the session
|
|
6
|
-
* through Phase R (rehydrate from SQL), Phase B (
|
|
7
|
-
*
|
|
8
|
-
* (cold-only) Phase O (MOTD + framework hint). The
|
|
9
|
-
* lots of locals (vfs, kernel, registry, shell) and
|
|
10
|
-
* matters strictly — there's no interesting reuse boundary
|
|
11
|
-
* class decomposition would expose.
|
|
6
|
+
* through Phase R (rehydrate from SQL), Phase B (compose the
|
|
7
|
+
* workspace + register the session's commands), Phase W (attach
|
|
8
|
+
* terminal), and (cold-only) Phase O (MOTD + framework hint). The
|
|
9
|
+
* phases share lots of locals (vfs, kernel, registry, shell) and
|
|
10
|
+
* ordering matters strictly — there's no interesting reuse boundary
|
|
11
|
+
* that a class decomposition would expose.
|
|
12
|
+
*
|
|
13
|
+
* What it no longer does is COMPOSE the operating system. The kernel,
|
|
14
|
+
* the provider mounts, the command registry, the coreutils, the exec
|
|
15
|
+
* resolver, the default environment and the shell come from
|
|
16
|
+
* `NimbusWorkspace` (@nimbus-sh/core/workspace), which an embedder off
|
|
17
|
+
* Cloudflare builds the same way. What stays here is everything that
|
|
18
|
+
* only makes sense with a socket, a Durable Object and a product behind
|
|
19
|
+
* it: the terminal and its scrollback, the phase machine, the persisted
|
|
20
|
+
* shell state, npm, git, vite, wrangler and the facet-backed runtimes.
|
|
12
21
|
*
|
|
13
22
|
* The function is intentionally written so that a reader sees:
|
|
14
23
|
* 1. setPhase('rehydrate') ...
|
|
@@ -38,6 +47,6 @@ type InitHost = SessionInternal & {
|
|
|
38
47
|
readonly ctx: any;
|
|
39
48
|
readonly env: any;
|
|
40
49
|
};
|
|
41
|
-
export declare function initSession(self: InitHost, ws: WebSocket): void
|
|
50
|
+
export declare function initSession(self: InitHost, ws: WebSocket): Promise<void>;
|
|
42
51
|
export {};
|
|
43
52
|
//# sourceMappingURL=init.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/session/init.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/session/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AA6EH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD;;;;;;;;GAQG;AACH,KAAK,QAAQ,GAAG,eAAe,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;CAAE,CAAC;AAa3E,wBAAsB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAsrF9E"}
|