@volter/teams 0.1.69 → 0.1.70
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/browser-channel.mjs +34 -16
- package/package.json +1 -1
package/browser-channel.mjs
CHANGED
|
@@ -104,14 +104,16 @@ export class BrowserChannel extends Emitter {
|
|
|
104
104
|
|
|
105
105
|
/** Open a named stream; resolves once the machine accepts it, rejects with its refusal. */
|
|
106
106
|
open(name, params = {}) {
|
|
107
|
+
if (this.closed || this.socket.readyState !== 1 || !this.hello) return Promise.reject(new Error('the machine channel is not connected'));
|
|
107
108
|
const id = this.next; this.next += 2;
|
|
108
109
|
const stream = new BrowserStream(this, id, name);
|
|
109
110
|
this.streams.set(id, stream);
|
|
110
111
|
return new Promise((resolve, reject) => {
|
|
111
112
|
// The refusal listener leaves once the stream opens, so a close after that is the caller's.
|
|
112
|
-
const
|
|
113
|
+
const timer = setTimeout(() => stream.close('stream opening timed out'), 15_000);
|
|
114
|
+
const refused = (reason) => { clearTimeout(timer); reject(new Error(`${name} refused: ${reason ?? 'closed'}`)); };
|
|
113
115
|
stream.on('close', refused);
|
|
114
|
-
stream.once('opened', () => { stream.off('close', refused); resolve(stream); });
|
|
116
|
+
stream.once('opened', () => { clearTimeout(timer); stream.off('close', refused); resolve(stream); });
|
|
115
117
|
this.send({ ch: id, t: 'open', name, params });
|
|
116
118
|
});
|
|
117
119
|
}
|
|
@@ -119,6 +121,30 @@ export class BrowserChannel extends Emitter {
|
|
|
119
121
|
close() { this.socket.close(1000, 'closed'); }
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
// The HTTP/WebSocket opening and the machine's hello are both required. A
|
|
125
|
+
// failed opening owns its cleanup; callers never inherit an abandoned socket.
|
|
126
|
+
function connectBrowserChannel(socket, channel, unreachable, closed) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
let settled = false;
|
|
129
|
+
const finish = error => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
settled = true; clearTimeout(timer);
|
|
132
|
+
channel.off('hello', hello);
|
|
133
|
+
socket.removeEventListener('open', opened); socket.removeEventListener('error', failed); socket.removeEventListener('close', ended);
|
|
134
|
+
if (error) { try { channel.close(); } finally { reject(error); } } else resolve(channel);
|
|
135
|
+
};
|
|
136
|
+
const opened = () => { try { socket.send(frame({ p: PROTOCOL, nv: NODE_PROTOCOL })); } catch (error) { finish(error); } };
|
|
137
|
+
const hello = value => finish(value.error ? new Error(value.error) : null);
|
|
138
|
+
const failed = () => finish(new Error(unreachable));
|
|
139
|
+
const ended = () => finish(new Error(closed));
|
|
140
|
+
const timer = setTimeout(() => finish(new Error('The machine channel did not connect before the deadline')), 15_000);
|
|
141
|
+
socket.addEventListener('open', opened, { once: true });
|
|
142
|
+
channel.on('hello', hello);
|
|
143
|
+
socket.addEventListener('error', failed, { once: true });
|
|
144
|
+
socket.addEventListener('close', ended, { once: true });
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
122
148
|
/**
|
|
123
149
|
* Machine `machineId` of team `teamId` on the Teams server at `origin`, reached with the
|
|
124
150
|
* caller's own `token`. `WebSocketImpl` defaults to the browser's WebSocket.
|
|
@@ -127,12 +153,7 @@ export function openBrowserMachine({ origin, teamId, machineId, token }, { WebSo
|
|
|
127
153
|
const url = `${String(origin).replace(/^http/, 'ws')}/api/v1/teams/${encodeURIComponent(teamId)}/machines/${encodeURIComponent(machineId)}/channel`;
|
|
128
154
|
const socket = new WebSocketImpl(url, [CHANNEL_PROTOCOL, `bearer.${token}`]);
|
|
129
155
|
const channel = new BrowserChannel(socket);
|
|
130
|
-
return
|
|
131
|
-
socket.addEventListener('open', () => socket.send(frame({ p: PROTOCOL, nv: NODE_PROTOCOL })), { once: true });
|
|
132
|
-
channel.once('hello', (hello) => (hello.error ? reject(new Error(hello.error)) : resolve(channel)));
|
|
133
|
-
socket.addEventListener('error', () => reject(new Error(`could not reach machine ${machineId} through ${origin}`)), { once: true });
|
|
134
|
-
socket.addEventListener('close', () => reject(new Error('the server closed the machine channel')), { once: true });
|
|
135
|
-
});
|
|
156
|
+
return connectBrowserChannel(socket, channel, `could not reach machine ${machineId} through ${origin}`, 'the server closed the machine channel');
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
/**
|
|
@@ -143,12 +164,7 @@ export function openBrowserSession({ origin, teamId, sessionId, token }, { WebSo
|
|
|
143
164
|
const url = `${String(origin).replace(/^http/, 'ws')}/api/v1/teams/${encodeURIComponent(teamId)}/sessions/${encodeURIComponent(sessionId)}/channel`;
|
|
144
165
|
const socket = new WebSocketImpl(url, [CHANNEL_PROTOCOL, `bearer.${token}`]);
|
|
145
166
|
const channel = new BrowserChannel(socket);
|
|
146
|
-
return
|
|
147
|
-
socket.addEventListener('open', () => socket.send(frame({ p: PROTOCOL, nv: NODE_PROTOCOL })), { once: true });
|
|
148
|
-
channel.once('hello', (hello) => (hello.error ? reject(new Error(hello.error)) : resolve(channel)));
|
|
149
|
-
socket.addEventListener('error', () => reject(new Error(`could not reach session ${sessionId} through ${origin}`)), { once: true });
|
|
150
|
-
socket.addEventListener('close', () => reject(new Error('the server closed the session channel')), { once: true });
|
|
151
|
-
});
|
|
167
|
+
return connectBrowserChannel(socket, channel, `could not reach session ${sessionId} through ${origin}`, 'the server closed the session channel');
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
/**
|
|
@@ -172,8 +188,10 @@ export async function readBrowserMail(channel) {
|
|
|
172
188
|
const stream = await channel.open('mail', { request: { op: 'history' } });
|
|
173
189
|
return new Promise((resolve, reject) => {
|
|
174
190
|
let answer = '';
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
// Reject before closing: close emits synchronously and its parser must
|
|
192
|
+
// not turn a timed-out or oversized response into a successful receipt.
|
|
193
|
+
const timer = setTimeout(() => { reject(new Error('Mailbox read timed out')); stream.close('mailbox read timed out'); }, 25_000);
|
|
194
|
+
stream.on('data', chunk => { answer += chunk; if (answer.length > 8_000_000) { reject(new Error('Mailbox response too large')); stream.close('mailbox response too large'); } });
|
|
177
195
|
stream.on('close', () => {
|
|
178
196
|
clearTimeout(timer);
|
|
179
197
|
try { const body = JSON.parse(answer); if (body.code !== 0) throw new Error(body.text || 'Mailbox unavailable'); resolve(body.rows); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/teams",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Volter Teams, the neutral team plane: machine identity and daemon core, the local door and server link, admission from machine grants, product doors, and the Teams server's neutral routes (company RFC 0008).",
|
|
6
6
|
"license": "MIT",
|