@componentor/fs 3.0.25 → 3.0.26

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.
@@ -267,20 +267,20 @@ function encodeData(data) {
267
267
  function encodeFdRequest(op, args) {
268
268
  switch (op) {
269
269
  case OP.FREAD: {
270
- const buf = new Uint8Array(12);
270
+ const buf = new Uint8Array(16);
271
271
  const view = new DataView(buf.buffer);
272
272
  view.setUint32(0, args.fd, true);
273
273
  view.setUint32(4, args.length ?? 0, true);
274
- view.setInt32(8, args.position ?? -1, true);
274
+ view.setFloat64(8, args.position ?? -1, true);
275
275
  return encodeRequest(op, "", 0, buf);
276
276
  }
277
277
  case OP.FWRITE: {
278
278
  const writeData = args.data ?? new Uint8Array(0);
279
- const buf = new Uint8Array(8 + writeData.byteLength);
279
+ const buf = new Uint8Array(12 + writeData.byteLength);
280
280
  const view = new DataView(buf.buffer);
281
281
  view.setUint32(0, args.fd, true);
282
- view.setInt32(4, args.position ?? -1, true);
283
- buf.set(writeData, 8);
282
+ view.setFloat64(4, args.position ?? -1, true);
283
+ buf.set(writeData, 12);
284
284
  return encodeRequest(op, "", 0, buf);
285
285
  }
286
286
  case OP.FSTAT:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/src/protocol/opcodes.ts","../../src/src/workers/async-relay.worker.ts"],"sourcesContent":["/**\n * Binary protocol operation codes and header encoding/decoding.\n * All inter-worker messages use this minimal binary protocol — no JSON, no strings.\n */\n\n// Operation codes\nexport const OP = {\n READ: 1,\n WRITE: 2,\n UNLINK: 3,\n STAT: 4,\n LSTAT: 5,\n MKDIR: 6,\n RMDIR: 7,\n READDIR: 8,\n RENAME: 9,\n EXISTS: 10,\n TRUNCATE: 11,\n APPEND: 12,\n COPY: 13,\n ACCESS: 14,\n REALPATH: 15,\n CHMOD: 16,\n CHOWN: 17,\n UTIMES: 18,\n SYMLINK: 19,\n READLINK: 20,\n LINK: 21,\n OPEN: 22,\n CLOSE: 23,\n FREAD: 24,\n FWRITE: 25,\n FSTAT: 26,\n FTRUNCATE: 27,\n FSYNC: 28,\n OPENDIR: 29,\n MKDTEMP: 30,\n} as const;\n\nexport type OpCode = (typeof OP)[keyof typeof OP];\n\n// Response status codes\nexport const STATUS = {\n OK: 0,\n ENOENT: 1,\n EEXIST: 2,\n EISDIR: 3,\n ENOTDIR: 4,\n ENOTEMPTY: 5,\n EACCES: 6,\n EINVAL: 7,\n EBADF: 8,\n ELOOP: 9,\n ENOSPC: 10,\n} as const;\n\n// SAB layout offsets\nexport const SAB_OFFSETS = {\n CONTROL: 0, // Int32 - signal (0=idle, 1=request, 2=response, 3=chunk, 4=ack)\n OPCODE: 4, // Int32 - operation code\n STATUS: 8, // Int32 - response status / error\n CHUNK_LEN: 12, // Int32 - bytes in this chunk\n TOTAL_LEN: 16, // BigUint64 - full data size across all chunks\n CHUNK_IDX: 24, // Int32 - 0-based chunk index\n RESERVED: 28, // Int32 - reserved\n HEADER_SIZE: 32, // Data payload starts here\n} as const;\n\n// SAB control signals\nexport const SIGNAL = {\n IDLE: 0,\n REQUEST: 1,\n RESPONSE: 2,\n CHUNK: 3,\n CHUNK_ACK: 4,\n} as const;\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/**\n * Encode a request into an ArrayBuffer for MessageChannel transfer.\n *\n * Request format (16-byte header + path + data):\n * bytes 0-3: operation (uint32)\n * bytes 4-7: flags (uint32)\n * bytes 8-11: pathLen (uint32)\n * bytes 12-15: dataLen (uint32)\n * bytes 16+: path (UTF-8)\n * bytes 16+pathLen: data payload\n */\nexport function encodeRequest(\n op: number,\n path: string,\n flags: number = 0,\n data?: Uint8Array\n): ArrayBuffer {\n const pathBytes = encoder.encode(path);\n const dataLen = data ? data.byteLength : 0;\n const totalLen = 16 + pathBytes.byteLength + dataLen;\n const buf = new ArrayBuffer(totalLen);\n const view = new DataView(buf);\n\n view.setUint32(0, op, true);\n view.setUint32(4, flags, true);\n view.setUint32(8, pathBytes.byteLength, true);\n view.setUint32(12, dataLen, true);\n\n const bytes = new Uint8Array(buf);\n bytes.set(pathBytes, 16);\n if (data) {\n bytes.set(data, 16 + pathBytes.byteLength);\n }\n\n return buf;\n}\n\n/**\n * Decode a request ArrayBuffer.\n */\nexport function decodeRequest(buf: ArrayBuffer): {\n op: number;\n flags: number;\n path: string;\n data: Uint8Array | null;\n} {\n // Minimum header: 16 bytes (op + flags + pathLen + dataLen)\n if (buf.byteLength < 16) {\n throw new Error(`Request buffer too small: ${buf.byteLength} < 16 bytes (possible SAB race)`);\n }\n\n const view = new DataView(buf);\n const op = view.getUint32(0, true);\n const flags = view.getUint32(4, true);\n const pathLen = view.getUint32(8, true);\n const dataLen = view.getUint32(12, true);\n\n // Validate payload fits in buffer\n const expectedMin = 16 + pathLen + dataLen;\n if (buf.byteLength < expectedMin) {\n throw new Error(`Request buffer truncated: ${buf.byteLength} < ${expectedMin} bytes (op=${op}, pathLen=${pathLen}, dataLen=${dataLen})`);\n }\n\n const bytes = new Uint8Array(buf);\n const path = decoder.decode(bytes.subarray(16, 16 + pathLen));\n const data = dataLen > 0\n ? bytes.subarray(16 + pathLen, 16 + pathLen + dataLen)\n : null;\n\n return { op, flags, path, data };\n}\n\n/**\n * Encode a response into an ArrayBuffer.\n *\n * Response format (8-byte header + data):\n * bytes 0-3: status (uint32)\n * bytes 4-7: dataLen (uint32)\n * bytes 8+: data payload\n */\nexport function encodeResponse(status: number, data?: Uint8Array): ArrayBuffer {\n const dataLen = data ? data.byteLength : 0;\n const buf = new ArrayBuffer(8 + dataLen);\n const view = new DataView(buf);\n\n view.setUint32(0, status, true);\n view.setUint32(4, dataLen, true);\n\n if (data) {\n new Uint8Array(buf).set(data, 8);\n }\n\n return buf;\n}\n\n/**\n * Decode a response ArrayBuffer.\n */\nexport function decodeResponse(buf: ArrayBuffer): {\n status: number;\n data: Uint8Array | null;\n} {\n const view = new DataView(buf);\n const status = view.getUint32(0, true);\n const dataLen = view.getUint32(4, true);\n\n const data = dataLen > 0\n ? new Uint8Array(buf, 8, dataLen)\n : null;\n\n return { status, data };\n}\n\n/**\n * Encode a two-path request (rename, copy, symlink, link).\n * Data payload contains: [pathLen2:u32] [path2 bytes]\n */\nexport function encodeTwoPathRequest(\n op: number,\n path1: string,\n path2: string,\n flags: number = 0\n): ArrayBuffer {\n const path2Bytes = encoder.encode(path2);\n const payload = new Uint8Array(4 + path2Bytes.byteLength);\n const pv = new DataView(payload.buffer);\n pv.setUint32(0, path2Bytes.byteLength, true);\n payload.set(path2Bytes, 4);\n\n return encodeRequest(op, path1, flags, payload);\n}\n\n/**\n * Decode the second path from a two-path request's data payload.\n */\nexport function decodeSecondPath(data: Uint8Array): string {\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n const pathLen = view.getUint32(0, true);\n return decoder.decode(data.subarray(4, 4 + pathLen));\n}\n","/**\n * Async Relay Worker — handles encoding/decoding off the main thread.\n *\n * Operates in one of two modes:\n *\n * LEADER MODE (primary tab):\n * - Communicates with own sync-relay via asyncSAB (SharedArrayBuffer)\n * - Uses Atomics.wait to block until sync-relay writes response\n * - No MessagePort hop — direct SAB-based communication\n *\n * FOLLOWER MODE (secondary tabs):\n * - Communicates with leader's sync-relay via MessagePort\n * - Same protocol as current server port communication\n * - Port is obtained through service worker tab discovery\n *\n * Both modes encode requests the same way (binary protocol) and decode\n * responses the same way. Only the transport differs.\n */\n\nimport {\n SAB_OFFSETS, SIGNAL,\n encodeRequest, encodeTwoPathRequest, decodeResponse,\n OP,\n} from '../protocol/opcodes.js';\n\nconst encoder = new TextEncoder();\nconst HEADER_SIZE = SAB_OFFSETS.HEADER_SIZE;\n\n// ========== Leader mode: asyncSAB communication ==========\n\nlet asyncSab: SharedArrayBuffer | null = null;\nlet asyncCtrl: Int32Array | null = null;\n\n// Wake hint: sync-relay's SAB ctrl — notify to wake leader loop immediately\nlet wakeCtrl: Int32Array | null = null;\n\n/**\n * Send a request via asyncSAB and block until response (leader mode).\n */\nfunction sabRequest(requestBuf: ArrayBuffer): { status: number; data: Uint8Array | null } {\n const maxChunk = asyncSab!.byteLength - HEADER_SIZE;\n const requestBytes = new Uint8Array(requestBuf);\n const totalLenView = new BigUint64Array(asyncSab!, SAB_OFFSETS.TOTAL_LEN, 1);\n\n // Write request to asyncSAB\n if (requestBytes.byteLength <= maxChunk) {\n // Fast path: single chunk\n new Uint8Array(asyncSab!, HEADER_SIZE, requestBytes.byteLength).set(requestBytes);\n Atomics.store(asyncCtrl!, 3, requestBytes.byteLength);\n Atomics.store(totalLenView, 0, BigInt(requestBytes.byteLength));\n Atomics.store(asyncCtrl!, 0, SIGNAL.REQUEST);\n Atomics.notify(asyncCtrl!, 0);\n // Wake the leader loop (which waits on syncSAB's ctrl, not asyncCtrl)\n if (wakeCtrl) Atomics.notify(wakeCtrl, 0);\n } else {\n // Multi-chunk request\n let sent = 0;\n while (sent < requestBytes.byteLength) {\n const chunkSize = Math.min(maxChunk, requestBytes.byteLength - sent);\n new Uint8Array(asyncSab!, HEADER_SIZE, chunkSize).set(\n requestBytes.subarray(sent, sent + chunkSize)\n );\n Atomics.store(asyncCtrl!, 3, chunkSize);\n Atomics.store(totalLenView, 0, BigInt(requestBytes.byteLength));\n Atomics.store(asyncCtrl!, 6, Math.floor(sent / maxChunk));\n\n if (sent === 0) {\n Atomics.store(asyncCtrl!, 0, SIGNAL.REQUEST);\n } else {\n Atomics.store(asyncCtrl!, 0, SIGNAL.CHUNK);\n }\n Atomics.notify(asyncCtrl!, 0);\n // Wake leader loop on first chunk\n if (sent === 0 && wakeCtrl) Atomics.notify(wakeCtrl, 0);\n\n sent += chunkSize;\n if (sent < requestBytes.byteLength) {\n // Wait for sync-relay to ack non-final chunk\n Atomics.wait(asyncCtrl!, 0, sent === chunkSize ? SIGNAL.REQUEST : SIGNAL.CHUNK);\n }\n }\n // Wait for sync-relay to ack the LAST chunk before looking for response.\n // Without this, ctrl[0] is still our SIGNAL.CHUNK and we can't distinguish\n // it from a response CHUNK signal.\n while (Atomics.load(asyncCtrl!, 0) === SIGNAL.CHUNK) {\n Atomics.wait(asyncCtrl!, 0, SIGNAL.CHUNK, 100);\n }\n }\n\n // Wait for sync-relay to write the response.\n // After single-chunk: ctrl transitions REQUEST → RESPONSE (or CHUNK for multi-response)\n // After multi-chunk: ctrl transitions CHUNK → CHUNK_ACK → RESPONSE (or CHUNK for multi-response)\n // At this point ctrl[0] is NOT our CHUNK (we waited above). It's either\n // CHUNK_ACK (sync still processing), RESPONSE (done), or CHUNK (multi-response first chunk).\n let signal: number;\n for (;;) {\n signal = Atomics.load(asyncCtrl!, 0);\n if (signal === SIGNAL.RESPONSE || signal === SIGNAL.CHUNK) break;\n Atomics.wait(asyncCtrl!, 0, signal, 1000);\n }\n\n // Read response (may be multi-chunk)\n const respChunkLen = Atomics.load(asyncCtrl!, 3);\n const respTotalLen = Number(Atomics.load(totalLenView, 0));\n\n let responseBytes: Uint8Array;\n\n if (signal === SIGNAL.RESPONSE && respTotalLen <= maxChunk) {\n // Single chunk response\n responseBytes = new Uint8Array(asyncSab!, HEADER_SIZE, respChunkLen).slice();\n } else {\n // Multi-chunk response\n responseBytes = new Uint8Array(respTotalLen);\n let received = 0;\n\n responseBytes.set(new Uint8Array(asyncSab!, HEADER_SIZE, respChunkLen), 0);\n received += respChunkLen;\n\n while (received < respTotalLen) {\n Atomics.store(asyncCtrl!, 0, SIGNAL.CHUNK_ACK);\n Atomics.notify(asyncCtrl!, 0);\n Atomics.wait(asyncCtrl!, 0, SIGNAL.CHUNK_ACK);\n\n const nextLen = Atomics.load(asyncCtrl!, 3);\n responseBytes.set(new Uint8Array(asyncSab!, HEADER_SIZE, nextLen), received);\n received += nextLen;\n }\n }\n\n // Reset to IDLE and notify sync-relay so it can proceed\n Atomics.store(asyncCtrl!, 0, SIGNAL.IDLE);\n Atomics.notify(asyncCtrl!, 0);\n\n return decodeResponse(responseBytes.buffer as ArrayBuffer);\n}\n\n// ========== Follower mode: MessagePort communication ==========\n\nlet leaderPort: MessagePort | null = null;\nconst pending = new Map<string, (response: ArrayBuffer) => void>();\nlet requestId = 0;\n\nfunction nextId(): string {\n return 'a' + (requestId++);\n}\n\nfunction portRequest(buffer: ArrayBuffer): Promise<{ status: number; data: Uint8Array | null }> {\n return new Promise(resolve => {\n const id = nextId();\n pending.set(id, (respBuf) => {\n resolve(decodeResponse(respBuf));\n });\n leaderPort!.postMessage({ id, buffer }, [buffer]);\n });\n}\n\n// ========== Unified request dispatch ==========\n\nasync function sendRequest(reqBuffer: ArrayBuffer): Promise<{ status: number; data: Uint8Array | null }> {\n if (asyncSab) {\n // Leader mode: SAB-based (synchronous in worker, wrapped in promise for uniform API)\n return sabRequest(reqBuffer);\n } else if (leaderPort) {\n // Follower mode: MessagePort-based\n return portRequest(reqBuffer);\n }\n return { status: 7, data: null }; // EINVAL — no channel\n}\n\n// ========== Main thread message handling ==========\n\nself.onmessage = async (e: MessageEvent) => {\n const msg = e.data;\n\n // --- Leader mode init (with SAB) ---\n if (msg.type === 'init-leader') {\n asyncSab = msg.asyncSab;\n asyncCtrl = new Int32Array(msg.asyncSab, 0, 8);\n if (msg.wakeSab) {\n wakeCtrl = new Int32Array(msg.wakeSab, 0, 1);\n }\n return;\n }\n\n // --- Port mode init (no SAB: communicate with sync-relay via MessagePort) ---\n if (msg.type === 'init-port') {\n const port = msg.port ?? e.ports[0];\n if (port) {\n leaderPort = port;\n leaderPort!.onmessage = (ev: MessageEvent) => {\n const { id, buffer } = ev.data;\n const resolve = pending.get(id);\n if (resolve) {\n pending.delete(id);\n resolve(buffer);\n }\n };\n leaderPort!.start();\n }\n return;\n }\n\n // --- Follower mode init ---\n if (msg.type === 'init-follower') {\n // Nothing to do yet — port arrives separately\n return;\n }\n\n // --- Leader port (follower mode) ---\n if (msg.type === 'leader-port') {\n leaderPort = msg.port;\n leaderPort!.onmessage = (ev: MessageEvent) => {\n const { id, buffer } = ev.data;\n const resolve = pending.get(id);\n if (resolve) {\n pending.delete(id);\n resolve(buffer);\n }\n };\n leaderPort!.start();\n return;\n }\n\n // --- Handle async fs operation request from main thread ---\n if (msg.type === 'request') {\n const { callId, op, path, data, flags, path2, fdArgs } = msg;\n\n try {\n let reqBuffer: ArrayBuffer;\n\n // Encode request based on operation type\n if (path2 !== undefined) {\n // Two-path operations (rename, copy, symlink, link)\n reqBuffer = encodeTwoPathRequest(op, path, path2, flags ?? 0);\n } else if (fdArgs) {\n // File descriptor operations\n reqBuffer = encodeFdRequest(op, fdArgs);\n } else {\n // Standard single-path operations\n const encodedData = encodeData(data);\n reqBuffer = encodeRequest(op, path ?? '', flags ?? 0, encodedData ?? undefined);\n }\n\n const { status, data: respData } = await sendRequest(reqBuffer);\n\n (self as unknown as Worker).postMessage(\n { type: 'response', callId, status, data: respData },\n respData ? [respData.buffer] : []\n );\n } catch (err) {\n (self as unknown as Worker).postMessage({\n type: 'response',\n callId,\n status: 7, // EINVAL\n data: null,\n error: (err as Error).message,\n });\n }\n }\n};\n\n// ========== Encoding helpers ==========\n\nfunction encodeData(data: unknown): Uint8Array | null {\n if (data === null || data === undefined) return null;\n if (data instanceof Uint8Array) return data;\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n if (typeof data === 'string') return encoder.encode(data);\n return null;\n}\n\nfunction encodeFdRequest(op: number, args: { fd: number; length?: number; position?: number; data?: Uint8Array }): ArrayBuffer {\n switch (op) {\n case OP.FREAD: {\n const buf = new Uint8Array(12);\n const view = new DataView(buf.buffer);\n view.setUint32(0, args.fd, true);\n view.setUint32(4, args.length ?? 0, true);\n view.setInt32(8, args.position ?? -1, true);\n return encodeRequest(op, '', 0, buf);\n }\n case OP.FWRITE: {\n const writeData = args.data ?? new Uint8Array(0);\n const buf = new Uint8Array(8 + writeData.byteLength);\n const view = new DataView(buf.buffer);\n view.setUint32(0, args.fd, true);\n view.setInt32(4, args.position ?? -1, true);\n buf.set(writeData, 8);\n return encodeRequest(op, '', 0, buf);\n }\n case OP.FSTAT:\n case OP.CLOSE: {\n const buf = new Uint8Array(4);\n new DataView(buf.buffer).setUint32(0, args.fd, true);\n return encodeRequest(op, '', 0, buf);\n }\n case OP.FTRUNCATE: {\n const buf = new Uint8Array(8);\n const view = new DataView(buf.buffer);\n view.setUint32(0, args.fd, true);\n view.setUint32(4, args.length ?? 0, true);\n return encodeRequest(op, '', 0, buf);\n }\n case OP.FSYNC:\n return encodeRequest(op, '', 0);\n default:\n return encodeRequest(op, '', 0);\n }\n}\n"],"mappings":"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","names":["encoder"]}
1
+ {"version":3,"sources":["../../src/src/protocol/opcodes.ts","../../src/src/workers/async-relay.worker.ts"],"sourcesContent":["/**\n * Binary protocol operation codes and header encoding/decoding.\n * All inter-worker messages use this minimal binary protocol — no JSON, no strings.\n */\n\n// Operation codes\nexport const OP = {\n READ: 1,\n WRITE: 2,\n UNLINK: 3,\n STAT: 4,\n LSTAT: 5,\n MKDIR: 6,\n RMDIR: 7,\n READDIR: 8,\n RENAME: 9,\n EXISTS: 10,\n TRUNCATE: 11,\n APPEND: 12,\n COPY: 13,\n ACCESS: 14,\n REALPATH: 15,\n CHMOD: 16,\n CHOWN: 17,\n UTIMES: 18,\n SYMLINK: 19,\n READLINK: 20,\n LINK: 21,\n OPEN: 22,\n CLOSE: 23,\n FREAD: 24,\n FWRITE: 25,\n FSTAT: 26,\n FTRUNCATE: 27,\n FSYNC: 28,\n OPENDIR: 29,\n MKDTEMP: 30,\n} as const;\n\nexport type OpCode = (typeof OP)[keyof typeof OP];\n\n// Response status codes\nexport const STATUS = {\n OK: 0,\n ENOENT: 1,\n EEXIST: 2,\n EISDIR: 3,\n ENOTDIR: 4,\n ENOTEMPTY: 5,\n EACCES: 6,\n EINVAL: 7,\n EBADF: 8,\n ELOOP: 9,\n ENOSPC: 10,\n} as const;\n\n// SAB layout offsets\nexport const SAB_OFFSETS = {\n CONTROL: 0, // Int32 - signal (0=idle, 1=request, 2=response, 3=chunk, 4=ack)\n OPCODE: 4, // Int32 - operation code\n STATUS: 8, // Int32 - response status / error\n CHUNK_LEN: 12, // Int32 - bytes in this chunk\n TOTAL_LEN: 16, // BigUint64 - full data size across all chunks\n CHUNK_IDX: 24, // Int32 - 0-based chunk index\n RESERVED: 28, // Int32 - reserved\n HEADER_SIZE: 32, // Data payload starts here\n} as const;\n\n// SAB control signals\nexport const SIGNAL = {\n IDLE: 0,\n REQUEST: 1,\n RESPONSE: 2,\n CHUNK: 3,\n CHUNK_ACK: 4,\n} as const;\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n/**\n * Encode a request into an ArrayBuffer for MessageChannel transfer.\n *\n * Request format (16-byte header + path + data):\n * bytes 0-3: operation (uint32)\n * bytes 4-7: flags (uint32)\n * bytes 8-11: pathLen (uint32)\n * bytes 12-15: dataLen (uint32)\n * bytes 16+: path (UTF-8)\n * bytes 16+pathLen: data payload\n */\nexport function encodeRequest(\n op: number,\n path: string,\n flags: number = 0,\n data?: Uint8Array\n): ArrayBuffer {\n const pathBytes = encoder.encode(path);\n const dataLen = data ? data.byteLength : 0;\n const totalLen = 16 + pathBytes.byteLength + dataLen;\n const buf = new ArrayBuffer(totalLen);\n const view = new DataView(buf);\n\n view.setUint32(0, op, true);\n view.setUint32(4, flags, true);\n view.setUint32(8, pathBytes.byteLength, true);\n view.setUint32(12, dataLen, true);\n\n const bytes = new Uint8Array(buf);\n bytes.set(pathBytes, 16);\n if (data) {\n bytes.set(data, 16 + pathBytes.byteLength);\n }\n\n return buf;\n}\n\n/**\n * Decode a request ArrayBuffer.\n */\nexport function decodeRequest(buf: ArrayBuffer): {\n op: number;\n flags: number;\n path: string;\n data: Uint8Array | null;\n} {\n // Minimum header: 16 bytes (op + flags + pathLen + dataLen)\n if (buf.byteLength < 16) {\n throw new Error(`Request buffer too small: ${buf.byteLength} < 16 bytes (possible SAB race)`);\n }\n\n const view = new DataView(buf);\n const op = view.getUint32(0, true);\n const flags = view.getUint32(4, true);\n const pathLen = view.getUint32(8, true);\n const dataLen = view.getUint32(12, true);\n\n // Validate payload fits in buffer\n const expectedMin = 16 + pathLen + dataLen;\n if (buf.byteLength < expectedMin) {\n throw new Error(`Request buffer truncated: ${buf.byteLength} < ${expectedMin} bytes (op=${op}, pathLen=${pathLen}, dataLen=${dataLen})`);\n }\n\n const bytes = new Uint8Array(buf);\n const path = decoder.decode(bytes.subarray(16, 16 + pathLen));\n const data = dataLen > 0\n ? bytes.subarray(16 + pathLen, 16 + pathLen + dataLen)\n : null;\n\n return { op, flags, path, data };\n}\n\n/**\n * Encode a response into an ArrayBuffer.\n *\n * Response format (8-byte header + data):\n * bytes 0-3: status (uint32)\n * bytes 4-7: dataLen (uint32)\n * bytes 8+: data payload\n */\nexport function encodeResponse(status: number, data?: Uint8Array): ArrayBuffer {\n const dataLen = data ? data.byteLength : 0;\n const buf = new ArrayBuffer(8 + dataLen);\n const view = new DataView(buf);\n\n view.setUint32(0, status, true);\n view.setUint32(4, dataLen, true);\n\n if (data) {\n new Uint8Array(buf).set(data, 8);\n }\n\n return buf;\n}\n\n/**\n * Decode a response ArrayBuffer.\n */\nexport function decodeResponse(buf: ArrayBuffer): {\n status: number;\n data: Uint8Array | null;\n} {\n const view = new DataView(buf);\n const status = view.getUint32(0, true);\n const dataLen = view.getUint32(4, true);\n\n const data = dataLen > 0\n ? new Uint8Array(buf, 8, dataLen)\n : null;\n\n return { status, data };\n}\n\n/**\n * Encode a two-path request (rename, copy, symlink, link).\n * Data payload contains: [pathLen2:u32] [path2 bytes]\n */\nexport function encodeTwoPathRequest(\n op: number,\n path1: string,\n path2: string,\n flags: number = 0\n): ArrayBuffer {\n const path2Bytes = encoder.encode(path2);\n const payload = new Uint8Array(4 + path2Bytes.byteLength);\n const pv = new DataView(payload.buffer);\n pv.setUint32(0, path2Bytes.byteLength, true);\n payload.set(path2Bytes, 4);\n\n return encodeRequest(op, path1, flags, payload);\n}\n\n/**\n * Decode the second path from a two-path request's data payload.\n */\nexport function decodeSecondPath(data: Uint8Array): string {\n const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n const pathLen = view.getUint32(0, true);\n return decoder.decode(data.subarray(4, 4 + pathLen));\n}\n","/**\n * Async Relay Worker — handles encoding/decoding off the main thread.\n *\n * Operates in one of two modes:\n *\n * LEADER MODE (primary tab):\n * - Communicates with own sync-relay via asyncSAB (SharedArrayBuffer)\n * - Uses Atomics.wait to block until sync-relay writes response\n * - No MessagePort hop — direct SAB-based communication\n *\n * FOLLOWER MODE (secondary tabs):\n * - Communicates with leader's sync-relay via MessagePort\n * - Same protocol as current server port communication\n * - Port is obtained through service worker tab discovery\n *\n * Both modes encode requests the same way (binary protocol) and decode\n * responses the same way. Only the transport differs.\n */\n\nimport {\n SAB_OFFSETS, SIGNAL,\n encodeRequest, encodeTwoPathRequest, decodeResponse,\n OP,\n} from '../protocol/opcodes.js';\n\nconst encoder = new TextEncoder();\nconst HEADER_SIZE = SAB_OFFSETS.HEADER_SIZE;\n\n// ========== Leader mode: asyncSAB communication ==========\n\nlet asyncSab: SharedArrayBuffer | null = null;\nlet asyncCtrl: Int32Array | null = null;\n\n// Wake hint: sync-relay's SAB ctrl — notify to wake leader loop immediately\nlet wakeCtrl: Int32Array | null = null;\n\n/**\n * Send a request via asyncSAB and block until response (leader mode).\n */\nfunction sabRequest(requestBuf: ArrayBuffer): { status: number; data: Uint8Array | null } {\n const maxChunk = asyncSab!.byteLength - HEADER_SIZE;\n const requestBytes = new Uint8Array(requestBuf);\n const totalLenView = new BigUint64Array(asyncSab!, SAB_OFFSETS.TOTAL_LEN, 1);\n\n // Write request to asyncSAB\n if (requestBytes.byteLength <= maxChunk) {\n // Fast path: single chunk\n new Uint8Array(asyncSab!, HEADER_SIZE, requestBytes.byteLength).set(requestBytes);\n Atomics.store(asyncCtrl!, 3, requestBytes.byteLength);\n Atomics.store(totalLenView, 0, BigInt(requestBytes.byteLength));\n Atomics.store(asyncCtrl!, 0, SIGNAL.REQUEST);\n Atomics.notify(asyncCtrl!, 0);\n // Wake the leader loop (which waits on syncSAB's ctrl, not asyncCtrl)\n if (wakeCtrl) Atomics.notify(wakeCtrl, 0);\n } else {\n // Multi-chunk request\n let sent = 0;\n while (sent < requestBytes.byteLength) {\n const chunkSize = Math.min(maxChunk, requestBytes.byteLength - sent);\n new Uint8Array(asyncSab!, HEADER_SIZE, chunkSize).set(\n requestBytes.subarray(sent, sent + chunkSize)\n );\n Atomics.store(asyncCtrl!, 3, chunkSize);\n Atomics.store(totalLenView, 0, BigInt(requestBytes.byteLength));\n Atomics.store(asyncCtrl!, 6, Math.floor(sent / maxChunk));\n\n if (sent === 0) {\n Atomics.store(asyncCtrl!, 0, SIGNAL.REQUEST);\n } else {\n Atomics.store(asyncCtrl!, 0, SIGNAL.CHUNK);\n }\n Atomics.notify(asyncCtrl!, 0);\n // Wake leader loop on first chunk\n if (sent === 0 && wakeCtrl) Atomics.notify(wakeCtrl, 0);\n\n sent += chunkSize;\n if (sent < requestBytes.byteLength) {\n // Wait for sync-relay to ack non-final chunk\n Atomics.wait(asyncCtrl!, 0, sent === chunkSize ? SIGNAL.REQUEST : SIGNAL.CHUNK);\n }\n }\n // Wait for sync-relay to ack the LAST chunk before looking for response.\n // Without this, ctrl[0] is still our SIGNAL.CHUNK and we can't distinguish\n // it from a response CHUNK signal.\n while (Atomics.load(asyncCtrl!, 0) === SIGNAL.CHUNK) {\n Atomics.wait(asyncCtrl!, 0, SIGNAL.CHUNK, 100);\n }\n }\n\n // Wait for sync-relay to write the response.\n // After single-chunk: ctrl transitions REQUEST → RESPONSE (or CHUNK for multi-response)\n // After multi-chunk: ctrl transitions CHUNK → CHUNK_ACK → RESPONSE (or CHUNK for multi-response)\n // At this point ctrl[0] is NOT our CHUNK (we waited above). It's either\n // CHUNK_ACK (sync still processing), RESPONSE (done), or CHUNK (multi-response first chunk).\n let signal: number;\n for (;;) {\n signal = Atomics.load(asyncCtrl!, 0);\n if (signal === SIGNAL.RESPONSE || signal === SIGNAL.CHUNK) break;\n Atomics.wait(asyncCtrl!, 0, signal, 1000);\n }\n\n // Read response (may be multi-chunk)\n const respChunkLen = Atomics.load(asyncCtrl!, 3);\n const respTotalLen = Number(Atomics.load(totalLenView, 0));\n\n let responseBytes: Uint8Array;\n\n if (signal === SIGNAL.RESPONSE && respTotalLen <= maxChunk) {\n // Single chunk response\n responseBytes = new Uint8Array(asyncSab!, HEADER_SIZE, respChunkLen).slice();\n } else {\n // Multi-chunk response\n responseBytes = new Uint8Array(respTotalLen);\n let received = 0;\n\n responseBytes.set(new Uint8Array(asyncSab!, HEADER_SIZE, respChunkLen), 0);\n received += respChunkLen;\n\n while (received < respTotalLen) {\n Atomics.store(asyncCtrl!, 0, SIGNAL.CHUNK_ACK);\n Atomics.notify(asyncCtrl!, 0);\n Atomics.wait(asyncCtrl!, 0, SIGNAL.CHUNK_ACK);\n\n const nextLen = Atomics.load(asyncCtrl!, 3);\n responseBytes.set(new Uint8Array(asyncSab!, HEADER_SIZE, nextLen), received);\n received += nextLen;\n }\n }\n\n // Reset to IDLE and notify sync-relay so it can proceed\n Atomics.store(asyncCtrl!, 0, SIGNAL.IDLE);\n Atomics.notify(asyncCtrl!, 0);\n\n return decodeResponse(responseBytes.buffer as ArrayBuffer);\n}\n\n// ========== Follower mode: MessagePort communication ==========\n\nlet leaderPort: MessagePort | null = null;\nconst pending = new Map<string, (response: ArrayBuffer) => void>();\nlet requestId = 0;\n\nfunction nextId(): string {\n return 'a' + (requestId++);\n}\n\nfunction portRequest(buffer: ArrayBuffer): Promise<{ status: number; data: Uint8Array | null }> {\n return new Promise(resolve => {\n const id = nextId();\n pending.set(id, (respBuf) => {\n resolve(decodeResponse(respBuf));\n });\n leaderPort!.postMessage({ id, buffer }, [buffer]);\n });\n}\n\n// ========== Unified request dispatch ==========\n\nasync function sendRequest(reqBuffer: ArrayBuffer): Promise<{ status: number; data: Uint8Array | null }> {\n if (asyncSab) {\n // Leader mode: SAB-based (synchronous in worker, wrapped in promise for uniform API)\n return sabRequest(reqBuffer);\n } else if (leaderPort) {\n // Follower mode: MessagePort-based\n return portRequest(reqBuffer);\n }\n return { status: 7, data: null }; // EINVAL — no channel\n}\n\n// ========== Main thread message handling ==========\n\nself.onmessage = async (e: MessageEvent) => {\n const msg = e.data;\n\n // --- Leader mode init (with SAB) ---\n if (msg.type === 'init-leader') {\n asyncSab = msg.asyncSab;\n asyncCtrl = new Int32Array(msg.asyncSab, 0, 8);\n if (msg.wakeSab) {\n wakeCtrl = new Int32Array(msg.wakeSab, 0, 1);\n }\n return;\n }\n\n // --- Port mode init (no SAB: communicate with sync-relay via MessagePort) ---\n if (msg.type === 'init-port') {\n const port = msg.port ?? e.ports[0];\n if (port) {\n leaderPort = port;\n leaderPort!.onmessage = (ev: MessageEvent) => {\n const { id, buffer } = ev.data;\n const resolve = pending.get(id);\n if (resolve) {\n pending.delete(id);\n resolve(buffer);\n }\n };\n leaderPort!.start();\n }\n return;\n }\n\n // --- Follower mode init ---\n if (msg.type === 'init-follower') {\n // Nothing to do yet — port arrives separately\n return;\n }\n\n // --- Leader port (follower mode) ---\n if (msg.type === 'leader-port') {\n leaderPort = msg.port;\n leaderPort!.onmessage = (ev: MessageEvent) => {\n const { id, buffer } = ev.data;\n const resolve = pending.get(id);\n if (resolve) {\n pending.delete(id);\n resolve(buffer);\n }\n };\n leaderPort!.start();\n return;\n }\n\n // --- Handle async fs operation request from main thread ---\n if (msg.type === 'request') {\n const { callId, op, path, data, flags, path2, fdArgs } = msg;\n\n try {\n let reqBuffer: ArrayBuffer;\n\n // Encode request based on operation type\n if (path2 !== undefined) {\n // Two-path operations (rename, copy, symlink, link)\n reqBuffer = encodeTwoPathRequest(op, path, path2, flags ?? 0);\n } else if (fdArgs) {\n // File descriptor operations\n reqBuffer = encodeFdRequest(op, fdArgs);\n } else {\n // Standard single-path operations\n const encodedData = encodeData(data);\n reqBuffer = encodeRequest(op, path ?? '', flags ?? 0, encodedData ?? undefined);\n }\n\n const { status, data: respData } = await sendRequest(reqBuffer);\n\n (self as unknown as Worker).postMessage(\n { type: 'response', callId, status, data: respData },\n respData ? [respData.buffer] : []\n );\n } catch (err) {\n (self as unknown as Worker).postMessage({\n type: 'response',\n callId,\n status: 7, // EINVAL\n data: null,\n error: (err as Error).message,\n });\n }\n }\n};\n\n// ========== Encoding helpers ==========\n\nfunction encodeData(data: unknown): Uint8Array | null {\n if (data === null || data === undefined) return null;\n if (data instanceof Uint8Array) return data;\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n if (typeof data === 'string') return encoder.encode(data);\n return null;\n}\n\nfunction encodeFdRequest(op: number, args: { fd: number; length?: number; position?: number; data?: Uint8Array }): ArrayBuffer {\n switch (op) {\n case OP.FREAD: {\n const buf = new Uint8Array(16);\n const view = new DataView(buf.buffer);\n view.setUint32(0, args.fd, true);\n view.setUint32(4, args.length ?? 0, true);\n view.setFloat64(8, args.position ?? -1, true);\n return encodeRequest(op, '', 0, buf);\n }\n case OP.FWRITE: {\n const writeData = args.data ?? new Uint8Array(0);\n const buf = new Uint8Array(12 + writeData.byteLength);\n const view = new DataView(buf.buffer);\n view.setUint32(0, args.fd, true);\n view.setFloat64(4, args.position ?? -1, true);\n buf.set(writeData, 12);\n return encodeRequest(op, '', 0, buf);\n }\n case OP.FSTAT:\n case OP.CLOSE: {\n const buf = new Uint8Array(4);\n new DataView(buf.buffer).setUint32(0, args.fd, true);\n return encodeRequest(op, '', 0, buf);\n }\n case OP.FTRUNCATE: {\n const buf = new Uint8Array(8);\n const view = new DataView(buf.buffer);\n view.setUint32(0, args.fd, true);\n view.setUint32(4, args.length ?? 0, true);\n return encodeRequest(op, '', 0, buf);\n }\n case OP.FSYNC:\n return encodeRequest(op, '', 0);\n default:\n return encodeRequest(op, '', 0);\n }\n}\n"],"mappings":"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","names":["encoder"]}
@@ -40,8 +40,8 @@ var INODE = {
40
40
  // uint32 - byte offset into path table
41
41
  PATH_LENGTH: 8,
42
42
  // uint16 - length of path string
43
- RESERVED_10: 10,
44
- // uint16
43
+ NLINK: 10,
44
+ // uint16 - hard link count
45
45
  MODE: 12,
46
46
  // uint32 - permissions (e.g. 0o100644)
47
47
  SIZE: 16,
@@ -415,6 +415,7 @@ var VFSEngine = class {
415
415
  type,
416
416
  pathOffset,
417
417
  pathLength,
418
+ nlink: inodeView.getUint16(off + INODE.NLINK, true) || 1,
418
419
  mode: inodeView.getUint32(off + INODE.MODE, true),
419
420
  size,
420
421
  firstBlock,
@@ -449,6 +450,7 @@ var VFSEngine = class {
449
450
  type: v.getUint8(INODE.TYPE),
450
451
  pathOffset: v.getUint32(INODE.PATH_OFFSET, true),
451
452
  pathLength: v.getUint16(INODE.PATH_LENGTH, true),
453
+ nlink: v.getUint16(INODE.NLINK, true) || 1,
452
454
  mode: v.getUint32(INODE.MODE, true),
453
455
  size: v.getFloat64(INODE.SIZE, true),
454
456
  firstBlock: v.getUint32(INODE.FIRST_BLOCK, true),
@@ -475,7 +477,7 @@ var VFSEngine = class {
475
477
  v.setUint8(INODE.FLAGS + 2, 0);
476
478
  v.setUint32(INODE.PATH_OFFSET, inode.pathOffset, true);
477
479
  v.setUint16(INODE.PATH_LENGTH, inode.pathLength, true);
478
- v.setUint16(INODE.RESERVED_10, 0, true);
480
+ v.setUint16(INODE.NLINK, inode.nlink, true);
479
481
  v.setUint32(INODE.MODE, inode.mode, true);
480
482
  v.setFloat64(INODE.SIZE, inode.size, true);
481
483
  v.setUint32(INODE.FIRST_BLOCK, inode.firstBlock, true);
@@ -717,6 +719,7 @@ var VFSEngine = class {
717
719
  type,
718
720
  pathOffset: pathOff,
719
721
  pathLength: pathLen,
722
+ nlink: type === INODE_TYPE.DIRECTORY ? 2 : 1,
720
723
  mode,
721
724
  size,
722
725
  firstBlock,
@@ -869,6 +872,7 @@ var VFSEngine = class {
869
872
  if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
870
873
  const inode = this.readInode(idx);
871
874
  if (inode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EISDIR };
875
+ inode.nlink = Math.max(0, inode.nlink - 1);
872
876
  this.freeBlockRange(inode.firstBlock, inode.blockCount);
873
877
  inode.type = INODE_TYPE.FREE;
874
878
  this.writeInode(idx, inode);
@@ -893,7 +897,21 @@ var VFSEngine = class {
893
897
  }
894
898
  encodeStatResponse(idx) {
895
899
  const inode = this.readInode(idx);
896
- const buf = new Uint8Array(49);
900
+ let nlink = inode.nlink;
901
+ if (inode.type === INODE_TYPE.DIRECTORY) {
902
+ const path = this.readPath(inode.pathOffset, inode.pathLength);
903
+ const children = this.getDirectChildren(path);
904
+ let subdirCount = 0;
905
+ for (const child of children) {
906
+ const childIdx = this.pathIndex.get(child);
907
+ if (childIdx !== void 0) {
908
+ const childInode = this.readInode(childIdx);
909
+ if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
910
+ }
911
+ }
912
+ nlink = 2 + subdirCount;
913
+ }
914
+ const buf = new Uint8Array(53);
897
915
  const view = new DataView(buf.buffer);
898
916
  view.setUint8(0, inode.type);
899
917
  view.setUint32(1, inode.mode, true);
@@ -904,6 +922,7 @@ var VFSEngine = class {
904
922
  view.setUint32(37, inode.uid, true);
905
923
  view.setUint32(41, inode.gid, true);
906
924
  view.setUint32(45, idx, true);
925
+ view.setUint32(49, nlink, true);
907
926
  return { status: 0, data: buf };
908
927
  }
909
928
  // ---- MKDIR ----
@@ -1216,9 +1235,26 @@ var VFSEngine = class {
1216
1235
  const target = this.readData(inode.firstBlock, inode.blockCount, inode.size);
1217
1236
  return { status: 0, data: target };
1218
1237
  }
1219
- // ---- LINK (hard link — copies the file) ----
1238
+ // ---- LINK (hard link — copies the file data, tracks nlink) ----
1220
1239
  link(existingPath, newPath) {
1221
- return this.copy(existingPath, newPath);
1240
+ existingPath = this.normalizePath(existingPath);
1241
+ newPath = this.normalizePath(newPath);
1242
+ const srcIdx = this.resolvePathComponents(existingPath, true);
1243
+ if (srcIdx === void 0) return { status: CODE_TO_STATUS.ENOENT };
1244
+ const srcInode = this.readInode(srcIdx);
1245
+ if (srcInode.type === INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.EPERM };
1246
+ if (this.pathIndex.has(newPath)) return { status: CODE_TO_STATUS.EEXIST };
1247
+ const result = this.copy(existingPath, newPath);
1248
+ if (result.status !== 0) return result;
1249
+ srcInode.nlink++;
1250
+ this.writeInode(srcIdx, srcInode);
1251
+ const destIdx = this.pathIndex.get(newPath);
1252
+ if (destIdx !== void 0) {
1253
+ const destInode = this.readInode(destIdx);
1254
+ destInode.nlink = srcInode.nlink;
1255
+ this.writeInode(destIdx, destInode);
1256
+ }
1257
+ return { status: 0 };
1222
1258
  }
1223
1259
  // ---- OPEN (file descriptor) ----
1224
1260
  open(path, flags, tabId) {