diff options
Diffstat (limited to 'app/web/client.mjs')
| -rw-r--r-- | app/web/client.mjs | 138 |
1 files changed, 0 insertions, 138 deletions
diff --git a/app/web/client.mjs b/app/web/client.mjs deleted file mode 100644 index 69f44d2..0000000 --- a/app/web/client.mjs +++ /dev/null @@ -1,138 +0,0 @@ -// Browser transport and file operations. All 9P serialization lives in WASM. -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); -export class Client { - static async connect(url = '/_cloud9/9p', user = 'user', tree = '') { - const { instance } = await WebAssembly.instantiateStreaming(fetch(new URL('./cloud9.wasm', import.meta.url)), {}); - const wsURL = new URL(url, location.href); - wsURL.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; - const socket = new WebSocket(wsURL); - socket.binaryType = 'arraybuffer'; - const client = new Client(instance.exports, socket); - try { - await new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error('Connection timed out')), 10000); - socket.onopen = () => { clearTimeout(timer); resolve(); }; - socket.onerror = () => { clearTimeout(timer); reject(new Error('Cannot connect to 9P bridge')); }; - }); - await client.ask('version'); - const u = encoder.encode(user), a = encoder.encode(tree); - client.stage(new Uint8Array([...u, ...a])); - await client.ask('attach', u.length, a.length); - return client; - } catch (error) { client.close(); throw error; } - } - constructor(wasm, socket) { - this.wasm = wasm; - this.socket = socket; - this.pending = null; - this.queue = Promise.resolve(); - this.closed = false; - wasm.init(); - socket.onmessage = ({ data }) => { - if (!this.pending) { this.close(); return; } - try { - this.stage(new Uint8Array(data)); - const kind = wasm.receive(data.byteLength); - if (!kind) return; - const result = { kind, data: this.data(), number: wasm.result_number(), directory: Boolean(wasm.result_directory()) }; - const pending = this.pending; - this.pending = null; - clearTimeout(pending.timer); - if (kind < 0) { - const error = new Error(decoder.decode(result.data)); - pending.reject(error); - if (wasm.is_dead()) this.abort(error); - } - else pending.resolve(result); - } catch (error) { this.abort(error); } - }; - socket.onclose = () => this.abort(new Error('Connection closed')); - } - data() { return new Uint8Array(this.wasm.memory.buffer, this.wasm.data_ptr(), this.wasm.data_len()).slice(); } - stage(bytes) { - if (bytes.length > this.wasm.input_capacity()) throw new Error('Input exceeds 64 KiB'); - new Uint8Array(this.wasm.memory.buffer, this.wasm.input_ptr(), bytes.length).set(bytes); - return bytes.length; - } - ask(operation, ...args) { - if (this.closed) return Promise.reject(new Error('Connection closed')); - if (this.pending) return Promise.reject(new Error('Request already in flight')); - if (this.wasm[operation](...args) < 0) return Promise.reject(new Error(decoder.decode(this.data()))); - return new Promise((resolve, reject) => { - const timer = setTimeout(() => this.abort(new Error('9P request timed out')), 15000); - this.pending = { resolve, reject, timer }; - try { - this.socket.send(new Uint8Array(this.wasm.memory.buffer, this.wasm.output_ptr(), this.wasm.output_len())); - this.wasm.sent(); - } catch (error) { this.abort(error); } - }); - } - abort(error) { - this.closed = true; - if (this.pending) { clearTimeout(this.pending.timer); this.pending.reject(error); this.pending = null; } - this.socket.close(); - } - close() { this.abort(new Error('Disconnected')); } - serial(operation) { - const result = this.queue.then(operation); - this.queue = result.catch(() => {}); - return result; - } - async withFile(path, operation) { - const names = path.split('/').filter(Boolean); - let live = false; - try { - // Walk in spec-sized batches, retaining an unopened root fid. - for (let index = 0; index < Math.max(1, names.length); index += 16) { - const batch = names.slice(index, index + 16); - const len = this.stage(encoder.encode(batch.join('/'))); - const result = await this.ask('walk', index === 0 ? 0 : 1, 1, len); - live = live || result.number > 0 || batch.length === 0; - if (result.number !== batch.length) throw new Error('Path does not exist'); - } - return await operation(); - } finally { - if (live && !this.closed) await this.ask('clunk', 1); - } - } - readPath(path) { - return this.serial(() => this.withFile(path, async () => { - const stat = JSON.parse(decoder.decode((await this.ask('stat', 1)).data)); - const opened = await this.ask('open', 1, 0); - const count = Math.min(this.wasm.max_read(), opened.number || Infinity); - let offset = 0; - const chunks = [], entries = []; - while (true) { - const { data } = await this.ask('read', 1, BigInt(offset), count); - if (!data.length) break; - offset += data.length; - if (offset > 8 * 1024 * 1024) throw new Error('Browser view is limited to 8 MiB'); - if (stat.directory) { - this.stage(data); - if (this.wasm.decode_directory(data.length) < 0) throw new Error(decoder.decode(this.data())); - entries.push(...JSON.parse(decoder.decode(this.data()))); - } else chunks.push(data); - } - const bytes = new Uint8Array(stat.directory ? 0 : offset); - let position = 0; - for (const chunk of chunks) { bytes.set(chunk, position); position += chunk.length; } - return { stat, entries, bytes }; - })); - } - writePath(path, bytes) { - if (!(bytes instanceof Uint8Array) || bytes.length > 8 * 1024 * 1024) return Promise.reject(new Error('Write is limited to 8 MiB')); - return this.serial(() => this.withFile(path, async () => { - const opened = await this.ask('open', 1, 1 | 16); // OWRITE | OTRUNC - const count = Math.min(this.wasm.max_write(), opened.number || Infinity); - let offset = 0; - while (offset < bytes.length) { - const len = this.stage(bytes.subarray(offset, offset + count)); - const result = await this.ask('write', 1, BigInt(offset), len); - if (!result.number) throw new Error('Server made no write progress'); - offset += result.number; - } - return offset; - })); - } -} |
