1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
// 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;
}));
}
}
|