summaryrefslogtreecommitdiff
path: root/app/web/client.mjs
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-19 23:28:22 -0300
committerGabriel Schneider <[email protected]>2026-09-19 23:28:22 -0300
commitba996acfcad1698adbf4a1834fe50e73b1c6cab9 (patch)
tree282ba00ce5b10d7416aecb9f2f0f0a439340a57d /app/web/client.mjs
parentb05abcba3ea09ea106ad28364c6e40a3ec31b890 (diff)
downloadcloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.tar.gz
cloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.zip
Rename programs: 9player -> 9ns, introspect -> 9proc, app -> web (9web)
Directories, binaries, build options (-D9ns, -D9proc), step names, module name (9proc), thread and fs names, env var NINEPLAYER_MOUNT -> NINE_MOUNT, docs and test scripts. Browser assets move to web/static. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'app/web/client.mjs')
-rw-r--r--app/web/client.mjs138
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;
- }));
- }
-}