@teaui/remote-control 1.17.16

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.
@@ -0,0 +1,14 @@
1
+ import type { Unsubscribe } from '@teaui/core';
2
+ import { type Result } from '@teaui/result';
3
+ type Listener<T> = (value: T) => void | Promise<void>;
4
+ /** Internal callback registrations. A detach function owns one registration. */
5
+ export declare class Callbacks<T> {
6
+ #private;
7
+ constructor(onRejected: (cause: unknown) => void);
8
+ subscribe(listener: Listener<T>): Unsubscribe;
9
+ /** Snapshot delivery: additions wait for the next event; removals take effect now. */
10
+ emit(value: T, isCurrent?: () => boolean): unknown[];
11
+ /** Use the same error policy for both broadcasts and targeted readiness notifications. */
12
+ invoke(listener: Listener<T>, value: T): Result<void, unknown>;
13
+ }
14
+ export {};
@@ -0,0 +1,45 @@
1
+ import { ok, err } from '@teaui/result';
2
+ /** Internal callback registrations. A detach function owns one registration. */
3
+ export class Callbacks {
4
+ #listeners = new Set();
5
+ #onRejected;
6
+ constructor(onRejected) {
7
+ this.#onRejected = onRejected;
8
+ }
9
+ subscribe(listener) {
10
+ const registration = value => listener(value);
11
+ this.#listeners.add(registration);
12
+ return () => {
13
+ this.#listeners.delete(registration);
14
+ };
15
+ }
16
+ /** Snapshot delivery: additions wait for the next event; removals take effect now. */
17
+ emit(value, isCurrent = () => true) {
18
+ const errors = [];
19
+ const snapshot = [...this.#listeners];
20
+ for (const listener of snapshot) {
21
+ if (!isCurrent())
22
+ break;
23
+ if (!this.#listeners.has(listener))
24
+ continue;
25
+ const result = this.invoke(listener, value);
26
+ if (!result.ok)
27
+ errors.push(result.error);
28
+ }
29
+ return errors;
30
+ }
31
+ /** Use the same error policy for both broadcasts and targeted readiness notifications. */
32
+ invoke(listener, value) {
33
+ try {
34
+ const pending = listener(value);
35
+ if (pending) {
36
+ void Promise.resolve(pending).catch(cause => this.#onRejected(cause));
37
+ }
38
+ return ok(undefined);
39
+ }
40
+ catch (cause) {
41
+ return err(cause);
42
+ }
43
+ }
44
+ }
45
+ //# sourceMappingURL=Callbacks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Callbacks.js","sourceRoot":"","sources":["../lib/Callbacks.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,EAAE,EAAE,GAAG,EAAc,MAAM,eAAe,CAAA;AAIlD,gFAAgF;AAChF,MAAM,OAAO,SAAS;IACpB,UAAU,GAAG,IAAI,GAAG,EAAe,CAAA;IACnC,WAAW,CAA0B;IAErC,YAAY,UAAoC;QAC9C,IAAI,CAAC,WAAW,GAAG,UAAU,CAAA;IAC/B,CAAC;IAED,SAAS,CAAC,QAAqB;QAC7B,MAAM,YAAY,GAAgB,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QAC1D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;QACjC,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;QACtC,CAAC,CAAA;IACH,CAAC;IAED,sFAAsF;IACtF,IAAI,CAAC,KAAQ,EAAE,YAA2B,GAAG,EAAE,CAAC,IAAI;QAClD,MAAM,MAAM,GAAc,EAAE,CAAA;QAC5B,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,CAAA;QACrC,KAAK,MAAM,QAAQ,IAAI,QAAQ,EAAE,CAAC;YAChC,IAAI,CAAC,SAAS,EAAE;gBAAE,MAAK;YACvB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAAE,SAAQ;YAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;YAC3C,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC3C,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC;IAED,0FAA0F;IAC1F,MAAM,CAAC,QAAqB,EAAE,KAAQ;QACpC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;YAC/B,IAAI,OAAO,EAAE,CAAC;gBACZ,KAAK,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;YACvE,CAAC;YACD,OAAO,EAAE,CAAC,SAAS,CAAC,CAAA;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,GAAG,CAAC,KAAK,CAAC,CAAA;QACnB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,43 @@
1
+ import { type SystemEvent, type EventSource, type Unsubscribe } from '@teaui/core';
2
+ import { type Result } from '@teaui/result';
3
+ import { type RemoteControlError } from './errors.js';
4
+ import type { RemoteControlOptions, RemoteControlAddress, SnapshotFormat } from './types.js';
5
+ /** A reusable input source with optional snapshot output. Registrations survive restart. */
6
+ export declare class RemoteControlServer implements EventSource {
7
+ #private;
8
+ constructor(options?: RemoteControlOptions);
9
+ /**
10
+ * Start listening, or join the current startup. When already listening, return
11
+ * its address. After close/failure, start a fresh run once prior cleanup ends.
12
+ * Startup failures resolve an error Result and notify onError; they do not reject.
13
+ */
14
+ listen(): Promise<Result<RemoteControlAddress, RemoteControlError>>;
15
+ /**
16
+ * Supply snapshots on demand without coupling this source to a screen. Replaces
17
+ * the previous provider; its detach function cannot remove a newer provider.
18
+ * The registration survives close/restart. Providers synchronously return ANSI.
19
+ */
20
+ setSnapshotProvider(provider: () => string): Unsubscribe;
21
+ /** Format and broadcast an ANSI snapshot. No queue/replay while stopped. */
22
+ sendSnapshot(format: SnapshotFormat, ansiSnapshot: string): void;
23
+ /** Register input delivery independently of whether the server is running. */
24
+ onEvents(listener: (event: SystemEvent) => void): Unsubscribe;
25
+ /**
26
+ * Notify once per successful run, until detached. A subscriber added while
27
+ * listening gets the current address in a microtask, unless that run stops.
28
+ */
29
+ onListening(listener: (address: RemoteControlAddress) => void | Promise<void>): Unsubscribe;
30
+ /** Future errors only. Registration survives close; no past failures are replayed. */
31
+ onError(listener: (error: RemoteControlError) => void | Promise<void>): Unsubscribe;
32
+ /** The current bound address only. Undefined before binding and after close. */
33
+ get url(): string | undefined;
34
+ get port(): number | undefined;
35
+ /** True when idle or closing; false while starting or listening. */
36
+ get closed(): boolean;
37
+ /**
38
+ * Cancel the current run immediately, without removing subscriptions. Wait for
39
+ * cleanup if needed. Cleanup failures resolve an error Result and notify onError.
40
+ * A following listen() waits for cleanup attempts, then starts a fresh run.
41
+ */
42
+ close(): Promise<Result<void, RemoteControlError>>;
43
+ }
@@ -0,0 +1,174 @@
1
+ import { removeAnsi, } from '@teaui/core';
2
+ import { ok, err } from '@teaui/result';
3
+ import { Callbacks } from './Callbacks.js';
4
+ import { Session } from './Session.js';
5
+ import { remoteError, } from './errors.js';
6
+ /** A reusable input source with optional snapshot output. Registrations survive restart. */
7
+ export class RemoteControlServer {
8
+ #options;
9
+ #session;
10
+ #snapshotProvider;
11
+ #closing = Promise.resolve(ok(undefined));
12
+ #events = new Callbacks(cause => this.#reportError(remoteError({ type: 'dispatch-failed', causes: [cause] })));
13
+ #listening = new Callbacks(cause => this.#reportError(remoteError({ type: 'callback-failed', callback: 'listening', cause })));
14
+ #errors = new Callbacks(cause => this.#warn(cause));
15
+ constructor(options = {}) {
16
+ this.#options = { ...options };
17
+ }
18
+ /**
19
+ * Start listening, or join the current startup. When already listening, return
20
+ * its address. After close/failure, start a fresh run once prior cleanup ends.
21
+ * Startup failures resolve an error Result and notify onError; they do not reject.
22
+ */
23
+ listen() {
24
+ if (this.#session)
25
+ return this.#session.ready;
26
+ const session = new Session(this.#options, {
27
+ dispatch: event => this.#dispatch(event),
28
+ snapshot: format => this.#snapshot(format),
29
+ failed: error => {
30
+ if (this.#session !== session)
31
+ return;
32
+ void this.close(); // Cleanup failures are reported through onError.
33
+ this.#reportError(error);
34
+ },
35
+ sendFailed: error => {
36
+ if (this.#session === session)
37
+ this.#reportError(error);
38
+ },
39
+ });
40
+ this.#session = session;
41
+ // A failed cleanup is reported by close(). Wait for all its attempts before
42
+ // starting again; a new bind must report its own result, not an old failure.
43
+ session.start(this.#closing.then(() => { }));
44
+ void session.ready.then(result => {
45
+ if (!result.ok || this.#session !== session)
46
+ return;
47
+ for (const cause of this.#listening.emit(result.value, () => this.#session === session)) {
48
+ this.#reportError(remoteError({ type: 'callback-failed', callback: 'listening', cause }));
49
+ }
50
+ });
51
+ return session.ready;
52
+ }
53
+ /** Synchronous delivery errors become dispatch-failed replies; later rejections use onError. */
54
+ #dispatch(event) {
55
+ const causes = this.#events.emit(event);
56
+ return causes.length
57
+ ? err(remoteError({ type: 'dispatch-failed', causes }))
58
+ : ok(undefined);
59
+ }
60
+ /**
61
+ * Supply snapshots on demand without coupling this source to a screen. Replaces
62
+ * the previous provider; its detach function cannot remove a newer provider.
63
+ * The registration survives close/restart. Providers synchronously return ANSI.
64
+ */
65
+ setSnapshotProvider(provider) {
66
+ const registration = () => provider();
67
+ this.#snapshotProvider = registration;
68
+ return () => {
69
+ if (this.#snapshotProvider === registration)
70
+ this.#snapshotProvider = undefined;
71
+ };
72
+ }
73
+ #snapshot(format) {
74
+ if (!this.#snapshotProvider)
75
+ return err(remoteError({ type: 'snapshot-unavailable' }));
76
+ try {
77
+ return ok(formatSnapshot(this.#snapshotProvider(), format));
78
+ }
79
+ catch (cause) {
80
+ return err(remoteError({ type: 'snapshot-failed', cause }));
81
+ }
82
+ }
83
+ /** Format and broadcast an ANSI snapshot. No queue/replay while stopped. */
84
+ sendSnapshot(format, ansiSnapshot) {
85
+ this.#session?.sendSnapshot(format, formatSnapshot(ansiSnapshot, format));
86
+ }
87
+ /** Register input delivery independently of whether the server is running. */
88
+ onEvents(listener) {
89
+ return this.#events.subscribe(listener);
90
+ }
91
+ /**
92
+ * Notify once per successful run, until detached. A subscriber added while
93
+ * listening gets the current address in a microtask, unless that run stops.
94
+ */
95
+ onListening(listener) {
96
+ let active = true;
97
+ let notified;
98
+ const notify = (address) => {
99
+ const session = this.#session;
100
+ if (!active || !session || notified === session)
101
+ return;
102
+ notified = session;
103
+ return listener(address);
104
+ };
105
+ const detach = this.#listening.subscribe(notify);
106
+ const session = this.#session;
107
+ const address = session?.address;
108
+ if (address)
109
+ queueMicrotask(() => {
110
+ if (this.#session !== session)
111
+ return;
112
+ const result = this.#listening.invoke(notify, address);
113
+ if (!result.ok)
114
+ this.#reportError(remoteError({
115
+ type: 'callback-failed',
116
+ callback: 'listening',
117
+ cause: result.error,
118
+ }));
119
+ });
120
+ return () => {
121
+ active = false;
122
+ detach();
123
+ };
124
+ }
125
+ /** Future errors only. Registration survives close; no past failures are replayed. */
126
+ onError(listener) {
127
+ return this.#errors.subscribe(listener);
128
+ }
129
+ #reportError(error) {
130
+ for (const failure of this.#errors.emit(error))
131
+ this.#warn(failure);
132
+ }
133
+ #warn(cause) {
134
+ // Do not recursively call a failing error callback. Node accepts warning text.
135
+ const error = remoteError({
136
+ type: 'callback-failed',
137
+ callback: 'error',
138
+ cause,
139
+ });
140
+ process.emitWarning(error.message);
141
+ }
142
+ /** The current bound address only. Undefined before binding and after close. */
143
+ get url() {
144
+ return this.#session?.address?.url;
145
+ }
146
+ get port() {
147
+ return this.#session?.address?.port;
148
+ }
149
+ /** True when idle or closing; false while starting or listening. */
150
+ get closed() {
151
+ return this.#session === undefined;
152
+ }
153
+ /**
154
+ * Cancel the current run immediately, without removing subscriptions. Wait for
155
+ * cleanup if needed. Cleanup failures resolve an error Result and notify onError.
156
+ * A following listen() waits for cleanup attempts, then starts a fresh run.
157
+ */
158
+ close() {
159
+ const session = this.#session;
160
+ if (!session)
161
+ return this.#closing;
162
+ this.#session = undefined;
163
+ this.#closing = session.close();
164
+ void this.#closing.then(result => {
165
+ if (!result.ok)
166
+ this.#reportError(result.error);
167
+ });
168
+ return this.#closing;
169
+ }
170
+ }
171
+ function formatSnapshot(snapshot, format) {
172
+ return format === 'plain' ? removeAnsi(snapshot) : snapshot;
173
+ }
174
+ //# sourceMappingURL=RemoteControl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RemoteControl.js","sourceRoot":"","sources":["../lib/RemoteControl.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { type SystemEvent } from '@teaui/core';
2
+ import { type Result } from '@teaui/result';
3
+ import { type RemoteControlError, type RequestError } from './errors.js';
4
+ import type { RemoteControlOptions, RemoteControlAddress, SnapshotFormat } from './types.js';
5
+ interface Callbacks {
6
+ dispatch(event: SystemEvent): Result<void, RequestError>;
7
+ snapshot(format: SnapshotFormat): Result<string, RequestError>;
8
+ failed(error: RemoteControlError): void;
9
+ sendFailed(error: RemoteControlError): void;
10
+ }
11
+ /** One listening run. Never reused; callbacks cannot access another run's resources. */
12
+ export declare class Session {
13
+ #private;
14
+ readonly ready: Promise<Result<RemoteControlAddress, RemoteControlError>>;
15
+ constructor(options: RemoteControlOptions, callbacks: Callbacks);
16
+ get address(): RemoteControlAddress | undefined;
17
+ start(afterCleanup: Promise<void>): void;
18
+ /** Unsolicited messages deliberately have no request sequence. */
19
+ sendSnapshot(format: SnapshotFormat, snapshot: string): void;
20
+ /** Stop now; attempt all cleanup tasks and return any failures as a Result. */
21
+ close(): Promise<Result<void, RemoteControlError>>;
22
+ }
23
+ export {};
@@ -0,0 +1,248 @@
1
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { ok, err } from '@teaui/result';
4
+ import { decodeMessage, errorReply } from './protocol.js';
5
+ import { remoteError, } from './errors.js';
6
+ const HOST = '127.0.0.1';
7
+ const MAX_PAYLOAD = 64 * 1024;
8
+ // Styled full-screen snapshots can be much larger than individual input events.
9
+ const MAX_BUFFERED_REPLY_BYTES = 8 * 1024 * 1024;
10
+ const OPEN = 1;
11
+ /** One listening run. Never reused; callbacks cannot access another run's resources. */
12
+ export class Session {
13
+ #options;
14
+ #callbacks;
15
+ #http;
16
+ #server;
17
+ #abort = new AbortController();
18
+ #stopped = false;
19
+ #setup;
20
+ #cleanup;
21
+ #address;
22
+ #resolve;
23
+ ready = new Promise(resolve => {
24
+ this.#resolve = resolve;
25
+ });
26
+ constructor(options, callbacks) {
27
+ this.#options = options;
28
+ this.#callbacks = callbacks;
29
+ }
30
+ get address() {
31
+ return this.#address;
32
+ }
33
+ start(afterCleanup) {
34
+ this.#setup = afterCleanup
35
+ .then(() => this.#bind())
36
+ .catch(cause => this.#fail(remoteError({ type: 'startup-failed', cause })));
37
+ }
38
+ #settle(result) {
39
+ this.#resolve?.(result);
40
+ this.#resolve = undefined;
41
+ }
42
+ async #bind() {
43
+ if (this.#stopped)
44
+ return;
45
+ const { port = 0 } = this.#options;
46
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
47
+ this.#fail(remoteError({ type: 'invalid-port', port }));
48
+ return;
49
+ }
50
+ const { WebSocketServer } = await import('ws');
51
+ if (this.#stopped)
52
+ return;
53
+ const token = randomBytes(32).toString('hex');
54
+ const secret = Buffer.from(token);
55
+ const http = createServer((_req, response) => {
56
+ response.writeHead(426);
57
+ response.end('WebSocket upgrade required');
58
+ });
59
+ this.#http = http;
60
+ // Keep an HTTP error handler even after ws removes its forwarding handlers.
61
+ http.on('error', cause => this.#transportFailed(cause));
62
+ const server = new WebSocketServer({
63
+ server: http,
64
+ maxPayload: MAX_PAYLOAD,
65
+ perMessageDeflate: false,
66
+ verifyClient: ({ req }) => {
67
+ if (this.#stopped || req.headers.origin !== undefined)
68
+ return false;
69
+ try {
70
+ const url = new URL(req.url ?? '/', `http://${HOST}`);
71
+ const candidate = Buffer.from(url.searchParams.get('token') ?? '');
72
+ return (url.pathname === '/' &&
73
+ candidate.length === secret.length &&
74
+ timingSafeEqual(candidate, secret));
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ },
80
+ });
81
+ this.#server = server;
82
+ server.on('error', cause => this.#transportFailed(cause));
83
+ server.on('connection', socket => this.#connect(socket));
84
+ server.once('listening', () => {
85
+ if (this.#stopped)
86
+ return;
87
+ const boundPort = http.address().port;
88
+ this.#address = Object.freeze({
89
+ port: boundPort,
90
+ url: `ws://${HOST}:${boundPort}/?token=${token}`,
91
+ });
92
+ this.#settle(ok(this.#address));
93
+ });
94
+ http.listen({ host: HOST, port, signal: this.#abort.signal });
95
+ }
96
+ #transportFailed(cause) {
97
+ this.#fail(remoteError({
98
+ type: this.#address ? 'server-failed' : 'startup-failed',
99
+ cause,
100
+ }));
101
+ }
102
+ #fail(error) {
103
+ if (this.#stopped)
104
+ return;
105
+ this.#settle(err(error));
106
+ this.#callbacks.failed(error);
107
+ }
108
+ #connect(socket) {
109
+ if (this.#stopped) {
110
+ socket.terminate();
111
+ return;
112
+ }
113
+ let sequence = 0;
114
+ // Invalid frames and oversized messages affect this client only.
115
+ socket.on('error', () => socket.terminate());
116
+ socket.on('message', (data, isBinary) => {
117
+ if (this.#stopped)
118
+ return;
119
+ sequence += 1;
120
+ const decoded = decodeMessage(isBinary ? '' : data.toString(), isBinary);
121
+ let result;
122
+ if (!decoded.ok) {
123
+ result = decoded;
124
+ }
125
+ else if (decoded.value.type === 'snapshot') {
126
+ const { format } = decoded.value;
127
+ const captured = this.#callbacks.snapshot(format);
128
+ result = captured.ok
129
+ ? ok({
130
+ type: 'snapshot',
131
+ sequence,
132
+ format,
133
+ snapshot: captured.value,
134
+ })
135
+ : captured;
136
+ }
137
+ else {
138
+ const delivered = this.#callbacks.dispatch(decoded.value);
139
+ result = delivered.ok ? ok({ type: 'ack', sequence }) : delivered;
140
+ }
141
+ const reply = result.ok
142
+ ? result.value
143
+ : errorReply(sequence, result.error);
144
+ // Transmission is not part of dispatch. Never relabel a send failure.
145
+ this.#send([socket], reply);
146
+ });
147
+ }
148
+ /** Unsolicited messages deliberately have no request sequence. */
149
+ sendSnapshot(format, snapshot) {
150
+ if (this.#stopped || !this.#address || !this.#server)
151
+ return;
152
+ this.#send(this.#server.clients, { type: 'snapshot', format, snapshot });
153
+ }
154
+ #send(sockets, reply) {
155
+ if (this.#stopped)
156
+ return;
157
+ const recipients = [...sockets].filter(socket => socket.readyState === OPEN);
158
+ if (!recipients.length)
159
+ return;
160
+ // One encoding for this send, shared by all recipients; never retained.
161
+ let text;
162
+ let bytes;
163
+ try {
164
+ text = JSON.stringify(reply);
165
+ bytes = Buffer.byteLength(text);
166
+ }
167
+ catch (error) {
168
+ for (const socket of recipients)
169
+ this.#sendFailed(socket, error);
170
+ return;
171
+ }
172
+ for (const socket of recipients) {
173
+ if (this.#stopped || socket.readyState !== OPEN)
174
+ continue;
175
+ try {
176
+ if (socket.bufferedAmount + bytes > MAX_BUFFERED_REPLY_BYTES) {
177
+ socket.terminate();
178
+ continue;
179
+ }
180
+ socket.send(text, error => {
181
+ if (error)
182
+ this.#sendFailed(socket, error);
183
+ });
184
+ }
185
+ catch (error) {
186
+ this.#sendFailed(socket, error);
187
+ }
188
+ }
189
+ }
190
+ #sendFailed(socket, error) {
191
+ if (this.#stopped)
192
+ return;
193
+ socket.terminate();
194
+ this.#callbacks.sendFailed(remoteError({ type: 'send-failed', cause: error }));
195
+ }
196
+ /** Stop now; attempt all cleanup tasks and return any failures as a Result. */
197
+ close() {
198
+ if (this.#cleanup)
199
+ return this.#cleanup;
200
+ this.#stopped = true;
201
+ this.#address = undefined;
202
+ const canceled = remoteError({ type: 'startup-canceled' });
203
+ this.#settle(err(canceled));
204
+ const tasks = this.#setup ? [this.#setup] : [];
205
+ const attempt = (fn) => {
206
+ try {
207
+ fn();
208
+ }
209
+ catch (error) {
210
+ tasks.push(Promise.reject(error));
211
+ }
212
+ };
213
+ if (this.#http) {
214
+ const http = this.#http;
215
+ tasks.push(new Promise((resolve, reject) => http.close(error => {
216
+ if (error &&
217
+ error.code !== 'ERR_SERVER_NOT_RUNNING')
218
+ reject(error);
219
+ else
220
+ resolve();
221
+ })));
222
+ attempt(() => http.closeAllConnections());
223
+ }
224
+ attempt(() => this.#abort.abort(canceled));
225
+ if (this.#server) {
226
+ const server = this.#server;
227
+ for (const socket of server.clients)
228
+ attempt(() => socket.terminate());
229
+ tasks.push(new Promise((resolve, reject) => server.close(error => {
230
+ if (error)
231
+ reject(error);
232
+ else
233
+ resolve();
234
+ })));
235
+ }
236
+ // Waiting for setup (not readiness) also covers cancellation during import.
237
+ this.#cleanup = Promise.allSettled(tasks).then(results => {
238
+ const failures = results
239
+ .filter(result => result.status === 'rejected')
240
+ .map(result => result.reason);
241
+ return failures.length
242
+ ? err(remoteError({ type: 'cleanup-failed', causes: failures }))
243
+ : ok(undefined);
244
+ });
245
+ return this.#cleanup;
246
+ }
247
+ }
248
+ //# sourceMappingURL=Session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Session.js","sourceRoot":"","sources":["../lib/Session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,eAAe,EAAC,MAAM,aAAa,CAAA;AACxD,OAAO,EAAC,YAAY,EAAoC,MAAM,WAAW,CAAA;AAIzE,OAAO,EAAC,EAAE,EAAE,GAAG,EAAc,MAAM,eAAe,CAAA;AAClD,OAAO,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,eAAe,CAAA;AACvD,OAAO,EACL,WAAW,GAGZ,MAAM,aAAa,CAAA;AAepB,MAAM,IAAI,GAAG,WAAW,CAAA;AACxB,MAAM,WAAW,GAAG,EAAE,GAAG,IAAI,CAAA;AAC7B,gFAAgF;AAChF,MAAM,wBAAwB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAA;AAChD,MAAM,IAAI,GAAG,CAAC,CAAA;AAEd,wFAAwF;AACxF,MAAM,OAAO,OAAO;IAClB,QAAQ,CAAsB;IAC9B,UAAU,CAAW;IACrB,KAAK,CAAS;IACd,OAAO,CAAkB;IACzB,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;IAC9B,QAAQ,GAAG,KAAK,CAAA;IAChB,MAAM,CAAgB;IACtB,QAAQ,CAA4C;IACpD,QAAQ,CAAuB;IAC/B,QAAQ,CAAqE;IACpE,KAAK,GAAG,IAAI,OAAO,CAE1B,OAAO,CAAC,EAAE;QACV,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;IACzB,CAAC,CAAC,CAAA;IAEF,YAAY,OAA6B,EAAE,SAAoB;QAC7D,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;QACvB,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;IAC7B,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;IAED,KAAK,CAAC,YAA2B;QAC/B,IAAI,CAAC,MAAM,GAAG,YAAY;aACvB,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;aACxB,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,EAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAC,CAAC,CAAC,CAAC,CAAA;IAC7E,CAAC;IAED,OAAO,CAAC,MAAwD;QAC9D,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAA;QACvB,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAA;IAC3B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QACzB,MAAM,EAAC,IAAI,GAAG,CAAC,EAAC,GAAG,IAAI,CAAC,QAAQ,CAAA;QAChC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;YACxD,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,EAAC,IAAI,EAAE,cAAc,EAAE,IAAI,EAAC,CAAC,CAAC,CAAA;YACrD,OAAM;QACR,CAAC;QACD,MAAM,EAAC,eAAe,EAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAA;QAC5C,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QACzB,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;QAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjC,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE;YAC3C,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;YACvB,QAAQ,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAA;QAC5C,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,4EAA4E;QAC5E,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAA;QACvD,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;YACjC,MAAM,EAAE,IAAI;YACZ,UAAU,EAAE,WAAW;YACvB,iBAAiB,EAAE,KAAK;YACxB,YAAY,EAAE,CAAC,EAAC,GAAG,EAAyB,EAAE,EAAE;gBAC9C,IAAI,IAAI,CAAC,QAAQ,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS;oBAAE,OAAO,KAAK,CAAA;gBACnE,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,UAAU,IAAI,EAAE,CAAC,CAAA;oBACrD,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;oBAClE,OAAO,CACL,GAAG,CAAC,QAAQ,KAAK,GAAG;wBACpB,SAAS,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;wBAClC,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC,CACnC,CAAA;gBACH,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,KAAK,CAAA;gBACd,CAAC;YACH,CAAC;SACF,CAAC,CAAA;QACF,IAAI,CAAC,OAAO,GAAG,MAAM,CAAA;QACrB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAA;QACzD,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;QACxD,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,EAAE;YAC5B,IAAI,IAAI,CAAC,QAAQ;gBAAE,OAAM;YACzB,MAAM,SAAS,GAAI,IAAI,CAAC,OAAO,EAAkB,CAAC,IAAI,CAAA;YACtD,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;gBAC5B,IAAI,EAAE,SAAS;gBACf,GAAG,EAAE,QAAQ,IAAI,IAAI,SAAS,WAAW,KAAK,EAAE;aACjD,CAAC,CAAA;YACF,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;QACjC,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,MAAM,CAAC,EAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAC,CAAC,CAAA;IAC7D,CAAC;IAED,gBAAgB,CAAC,KAAc;QAC7B,IAAI,CAAC,KAAK,CACR,WAAW,CAAC;YACV,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,gBAAgB;YACxD,KAAK;SACN,CAAC,CACH,CAAA;IACH,CAAC;IAED,KAAK,CAAC,KAAyB;QAC7B,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;QACxB,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC/B,CAAC;IAED,QAAQ,CAAC,MAAiB;QACxB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,CAAC,SAAS,EAAE,CAAA;YAClB,OAAM;QACR,CAAC;QACD,IAAI,QAAQ,GAAG,CAAC,CAAA;QAChB,iEAAiE;QACjE,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAA;QAC5C,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE;YACtC,IAAI,IAAI,CAAC,QAAQ;gBAAE,OAAM;YACzB,QAAQ,IAAI,CAAC,CAAA;YACb,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAA;YACxE,IAAI,MAAgD,CAAA;YACpD,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;gBAChB,MAAM,GAAG,OAAO,CAAA;YAClB,CAAC;iBAAM,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC7C,MAAM,EAAC,MAAM,EAAC,GAAG,OAAO,CAAC,KAAK,CAAA;gBAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;gBACjD,MAAM,GAAG,QAAQ,CAAC,EAAE;oBAClB,CAAC,CAAC,EAAE,CAAC;wBACD,IAAI,EAAE,UAAU;wBAChB,QAAQ;wBACR,MAAM;wBACN,QAAQ,EAAE,QAAQ,CAAC,KAAK;qBACzB,CAAC;oBACJ,CAAC,CAAC,QAAQ,CAAA;YACd,CAAC;iBAAM,CAAC;gBACN,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;gBACzD,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;YACjE,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE;gBACrB,CAAC,CAAC,MAAM,CAAC,KAAK;gBACd,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAA;YACtC,sEAAsE;YACtE,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAA;QAC7B,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,kEAAkE;IAClE,YAAY,CAAC,MAAsB,EAAE,QAAgB;QACnD,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QAC5D,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAC,CAAC,CAAA;IACxE,CAAC;IAED,KAAK,CAAC,OAA4B,EAAE,KAAyB;QAC3D,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QACzB,MAAM,UAAU,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,UAAU,KAAK,IAAI,CAAC,CAAA;QAC5E,IAAI,CAAC,UAAU,CAAC,MAAM;YAAE,OAAM;QAE9B,wEAAwE;QACxE,IAAI,IAAY,CAAA;QAChB,IAAI,KAAa,CAAA;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;YAC5B,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QACjC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,MAAM,MAAM,IAAI,UAAU;gBAAE,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;YAChE,OAAM;QACR,CAAC;QAED,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;YAChC,IAAI,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI;gBAAE,SAAQ;YACzD,IAAI,CAAC;gBACH,IAAI,MAAM,CAAC,cAAc,GAAG,KAAK,GAAG,wBAAwB,EAAE,CAAC;oBAC7D,MAAM,CAAC,SAAS,EAAE,CAAA;oBAClB,SAAQ;gBACV,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE;oBACxB,IAAI,KAAK;wBAAE,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;gBAC5C,CAAC,CAAC,CAAA;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;YACjC,CAAC;QACH,CAAC;IACH,CAAC;IAED,WAAW,CAAC,MAAiB,EAAE,KAAc;QAC3C,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAM;QACzB,MAAM,CAAC,SAAS,EAAE,CAAA;QAClB,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,WAAW,CAAC,EAAC,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAC,CAAC,CAAC,CAAA;IAC9E,CAAC;IAED,+EAA+E;IAC/E,KAAK;QACH,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAA;QACvC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;QACpB,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAA;QACzB,MAAM,QAAQ,GAAG,WAAW,CAAC,EAAC,IAAI,EAAE,kBAAkB,EAAC,CAAC,CAAA;QACxD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;QAC3B,MAAM,KAAK,GAAoB,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAC/D,MAAM,OAAO,GAAG,CAAC,EAAc,EAAE,EAAE;YACjC,IAAI,CAAC;gBACH,EAAE,EAAE,CAAA;YACN,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;YACnC,CAAC;QACH,CAAC,CAAA;QACD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAA;YACvB,KAAK,CAAC,IAAI,CACR,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CACpC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;gBACjB,IACE,KAAK;oBACJ,KAA+B,CAAC,IAAI,KAAK,wBAAwB;oBAElE,MAAM,CAAC,KAAK,CAAC,CAAA;;oBACV,OAAO,EAAE,CAAA;YAChB,CAAC,CAAC,CACH,CACF,CAAA;YACD,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC,CAAA;QAC3C,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAA;QAC1C,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAA;YAC3B,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO;gBAAE,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAA;YACtE,KAAK,CAAC,IAAI,CACR,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CACpC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;gBACnB,IAAI,KAAK;oBAAE,MAAM,CAAC,KAAK,CAAC,CAAA;;oBACnB,OAAO,EAAE,CAAA;YAChB,CAAC,CAAC,CACH,CACF,CAAA;QACH,CAAC;QACD,4EAA4E;QAC5E,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;YACvD,MAAM,QAAQ,GAAG,OAAO;iBACrB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,UAAU,CAAC;iBAC9C,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAC/B,OAAO,QAAQ,CAAC,MAAM;gBACpB,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAC,CAAC,CAAC;gBAC9D,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAA;QACnB,CAAC,CAAC,CAAA;QACF,OAAO,IAAI,CAAC,QAAQ,CAAA;IACtB,CAAC;CACF"}
@@ -0,0 +1,50 @@
1
+ type RequestErrorDetail = {
2
+ readonly type: 'invalid-json';
3
+ } | {
4
+ readonly type: 'invalid-event';
5
+ readonly reason: 'shape' | 'snapshot-format';
6
+ } | {
7
+ readonly type: 'binary-message';
8
+ } | {
9
+ readonly type: 'dispatch-failed';
10
+ readonly causes: readonly unknown[];
11
+ } | {
12
+ readonly type: 'snapshot-unavailable';
13
+ } | {
14
+ readonly type: 'snapshot-failed';
15
+ readonly cause: unknown;
16
+ };
17
+ type ErrorDetail = RequestErrorDetail | {
18
+ readonly type: 'invalid-port';
19
+ readonly port: number;
20
+ } | {
21
+ readonly type: 'startup-canceled';
22
+ } | {
23
+ readonly type: 'startup-failed';
24
+ readonly cause: unknown;
25
+ } | {
26
+ readonly type: 'server-failed';
27
+ readonly cause: unknown;
28
+ } | {
29
+ readonly type: 'send-failed';
30
+ readonly cause: unknown;
31
+ } | {
32
+ readonly type: 'cleanup-failed';
33
+ readonly causes: readonly unknown[];
34
+ } | {
35
+ readonly type: 'callback-failed';
36
+ readonly callback: 'listening' | 'error';
37
+ readonly cause: unknown;
38
+ };
39
+ /** Plain error values, discriminated by type. Original failures remain local causes. */
40
+ export type RemoteControlError = ErrorDetail & {
41
+ readonly message: string;
42
+ };
43
+ export type RequestError = RequestErrorDetail & {
44
+ readonly message: string;
45
+ };
46
+ /** The one place that assigns messages to remote-control failures. */
47
+ export declare function remoteError<T extends ErrorDetail>(detail: T): T & {
48
+ readonly message: string;
49
+ };
50
+ export {};
@@ -0,0 +1,55 @@
1
+ /** The one place that assigns messages to remote-control failures. */
2
+ export function remoteError(detail) {
3
+ return { ...detail, message: errorMessage(detail) };
4
+ }
5
+ function errorMessage(error) {
6
+ switch (error.type) {
7
+ case 'invalid-json':
8
+ return 'Invalid JSON';
9
+ case 'invalid-event':
10
+ return error.reason === 'snapshot-format'
11
+ ? "Snapshot format must be 'plain' or 'ansi'"
12
+ : 'Expected a SystemEvent (key, mouse, paste, focus, blur, or resize) or snapshot request';
13
+ case 'binary-message':
14
+ return 'Send a SystemEvent or snapshot request as a JSON text message';
15
+ case 'dispatch-failed':
16
+ return error.causes.length === 1
17
+ ? causeMessage(error.causes[0], 'Remote control input callback failed')
18
+ : 'Remote control input callbacks failed';
19
+ case 'snapshot-unavailable':
20
+ return 'No snapshot provider registered';
21
+ case 'snapshot-failed':
22
+ return causeMessage(error.cause, 'Remote control snapshot failed');
23
+ case 'invalid-port':
24
+ return 'Remote control port must be an integer from 0 to 65535';
25
+ case 'startup-canceled':
26
+ return 'Remote control startup canceled';
27
+ case 'startup-failed':
28
+ return causeMessage(error.cause, 'Remote control setup failed');
29
+ case 'server-failed':
30
+ return causeMessage(error.cause, 'Remote control server failed');
31
+ case 'send-failed':
32
+ return causeMessage(error.cause, 'Remote control reply failed');
33
+ case 'cleanup-failed':
34
+ return 'Remote control cleanup failed';
35
+ case 'callback-failed':
36
+ return `Remote control ${error.callback} callback failed: ${causeMessage(error.cause, 'unknown cause')}`;
37
+ }
38
+ }
39
+ // Never coerce an arbitrary thrown value. Even reading a message can throw.
40
+ function causeMessage(cause, fallback) {
41
+ if (typeof cause === 'string')
42
+ return cause;
43
+ try {
44
+ if (typeof cause === 'object' && cause !== null && 'message' in cause) {
45
+ const message = cause.message;
46
+ if (typeof message === 'string')
47
+ return message;
48
+ }
49
+ }
50
+ catch {
51
+ // A throwing getter/proxy must not escape the error boundary.
52
+ }
53
+ return fallback;
54
+ }
55
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../lib/errors.ts"],"names":[],"mappings":"AA6BA,sEAAsE;AACtE,MAAM,UAAU,WAAW,CACzB,MAAS;IAET,OAAO,EAAC,GAAG,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,EAAC,CAAA;AACnD,CAAC;AAED,SAAS,YAAY,CAAC,KAAkB;IACtC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,cAAc;YACjB,OAAO,cAAc,CAAA;QACvB,KAAK,eAAe;YAClB,OAAO,KAAK,CAAC,MAAM,KAAK,iBAAiB;gBACvC,CAAC,CAAC,2CAA2C;gBAC7C,CAAC,CAAC,wFAAwF,CAAA;QAC9F,KAAK,gBAAgB;YACnB,OAAO,+DAA+D,CAAA;QACxE,KAAK,iBAAiB;YACpB,OAAO,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC;gBAC9B,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,sCAAsC,CAAC;gBACvE,CAAC,CAAC,uCAAuC,CAAA;QAC7C,KAAK,sBAAsB;YACzB,OAAO,iCAAiC,CAAA;QAC1C,KAAK,iBAAiB;YACpB,OAAO,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,gCAAgC,CAAC,CAAA;QACpE,KAAK,cAAc;YACjB,OAAO,wDAAwD,CAAA;QACjE,KAAK,kBAAkB;YACrB,OAAO,iCAAiC,CAAA;QAC1C,KAAK,gBAAgB;YACnB,OAAO,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,6BAA6B,CAAC,CAAA;QACjE,KAAK,eAAe;YAClB,OAAO,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,8BAA8B,CAAC,CAAA;QAClE,KAAK,aAAa;YAChB,OAAO,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,6BAA6B,CAAC,CAAA;QACjE,KAAK,gBAAgB;YACnB,OAAO,+BAA+B,CAAA;QACxC,KAAK,iBAAiB;YACpB,OAAO,kBAAkB,KAAK,CAAC,QAAQ,qBAAqB,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,eAAe,CAAC,EAAE,CAAA;IAC5G,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,SAAS,YAAY,CAAC,KAAc,EAAE,QAAgB;IACpD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,CAAC;QACH,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS,IAAI,KAAK,EAAE,CAAC;YACtE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;YAC7B,IAAI,OAAO,OAAO,KAAK,QAAQ;gBAAE,OAAO,OAAO,CAAA;QACjD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;IAChE,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { RemoteControlServer } from './RemoteControl.js';
2
+ export type { RemoteControlError } from './errors.js';
3
+ export type { RemoteControlOptions, RemoteControlAddress, RemoteControlRequest, RemoteControlReply, SnapshotFormat, SnapshotRequest, SnapshotMessage, } from './types.js';
package/.dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { RemoteControlServer } from './RemoteControl.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,mBAAmB,EAAC,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,9 @@
1
+ import { type Result } from '@teaui/result';
2
+ import type { RemoteControlRequest, RemoteControlReply } from './types.js';
3
+ import { type RequestError } from './errors.js';
4
+ /** Only public wire fields leave the process; never serialize local causes. */
5
+ export declare function errorReply(sequence: number, error: RequestError): Extract<RemoteControlReply, {
6
+ type: 'error';
7
+ }>;
8
+ /** Decode one message. This function has no socket or subscriber side effects. */
9
+ export declare function decodeMessage(text: string, isBinary: boolean): Result<RemoteControlRequest, RequestError>;
@@ -0,0 +1,32 @@
1
+ import { isSystemEvent } from './validate.js';
2
+ import { ok, err } from '@teaui/result';
3
+ import { remoteError } from './errors.js';
4
+ /** Only public wire fields leave the process; never serialize local causes. */
5
+ export function errorReply(sequence, error) {
6
+ return { type: 'error', sequence, code: error.type, message: error.message };
7
+ }
8
+ /** Decode one message. This function has no socket or subscriber side effects. */
9
+ export function decodeMessage(text, isBinary) {
10
+ if (isBinary)
11
+ return err(remoteError({ type: 'binary-message' }));
12
+ let event;
13
+ try {
14
+ event = JSON.parse(text);
15
+ }
16
+ catch {
17
+ return err(remoteError({ type: 'invalid-json' }));
18
+ }
19
+ if (typeof event === 'object' &&
20
+ event !== null &&
21
+ !Array.isArray(event) &&
22
+ event.type === 'snapshot') {
23
+ const format = event.format;
24
+ if (format === 'plain' || format === 'ansi')
25
+ return ok({ type: 'snapshot', format });
26
+ return err(remoteError({ type: 'invalid-event', reason: 'snapshot-format' }));
27
+ }
28
+ return isSystemEvent(event)
29
+ ? ok(event)
30
+ : err(remoteError({ type: 'invalid-event', reason: 'shape' }));
31
+ }
32
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../lib/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,aAAa,EAAC,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAC,EAAE,EAAE,GAAG,EAAc,MAAM,eAAe,CAAA;AAElD,OAAO,EAAC,WAAW,EAAoB,MAAM,aAAa,CAAA;AAE1D,+EAA+E;AAC/E,MAAM,UAAU,UAAU,CACxB,QAAgB,EAChB,KAAmB;IAEnB,OAAO,EAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAC,CAAA;AAC5E,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,aAAa,CAC3B,IAAY,EACZ,QAAiB;IAEjB,IAAI,QAAQ;QAAE,OAAO,GAAG,CAAC,WAAW,CAAC,EAAC,IAAI,EAAE,gBAAgB,EAAC,CAAC,CAAC,CAAA;IAC/D,IAAI,KAAc,CAAA;IAClB,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC,WAAW,CAAC,EAAC,IAAI,EAAE,cAAc,EAAC,CAAC,CAAC,CAAA;IACjD,CAAC;IACD,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACpB,KAAiC,CAAC,IAAI,KAAK,UAAU,EACtD,CAAC;QACD,MAAM,MAAM,GAAI,KAAiC,CAAC,MAAM,CAAA;QACxD,IAAI,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,MAAM;YACzC,OAAO,EAAE,CAAC,EAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAC,CAAC,CAAA;QACvC,OAAO,GAAG,CAAC,WAAW,CAAC,EAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,iBAAiB,EAAC,CAAC,CAAC,CAAA;IAC7E,CAAC;IACD,OAAO,aAAa,CAAC,KAAK,CAAC;QACzB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC;QACX,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,OAAO,EAAC,CAAC,CAAC,CAAA;AAChE,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { SystemEvent } from '@teaui/core';
2
+ import type { RequestError } from './errors.js';
3
+ export interface RemoteControlOptions {
4
+ /** Loopback port. Defaults to 0 (let the OS choose an unused port). */
5
+ port?: number;
6
+ }
7
+ export interface RemoteControlAddress {
8
+ readonly url: string;
9
+ readonly port: number;
10
+ }
11
+ export type SnapshotFormat = 'plain' | 'ansi';
12
+ export interface SnapshotRequest {
13
+ type: 'snapshot';
14
+ format: SnapshotFormat;
15
+ }
16
+ export type RemoteControlRequest = SystemEvent | SnapshotRequest;
17
+ export interface SnapshotMessage {
18
+ type: 'snapshot';
19
+ /** Request sequence, or absent for an application-initiated broadcast. */
20
+ sequence?: number;
21
+ format: SnapshotFormat;
22
+ snapshot: string;
23
+ }
24
+ export type RemoteControlReply = {
25
+ type: 'ack';
26
+ sequence: number;
27
+ } | {
28
+ type: 'error';
29
+ sequence: number;
30
+ code: RequestError['type'];
31
+ message: string;
32
+ } | SnapshotMessage;
package/.dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ import type { SystemEvent } from '@teaui/core';
2
+ /** Validate untrusted JSON before passing it to Screen.dispatch(). */
3
+ export declare function isSystemEvent(value: unknown): value is SystemEvent;
@@ -0,0 +1,48 @@
1
+ const MOUSE_NAMES = new Set([
2
+ 'mouse.move.in',
3
+ 'mouse.button.down',
4
+ 'mouse.button.up',
5
+ 'mouse.wheel.up',
6
+ 'mouse.wheel.down',
7
+ 'mouse.wheel.left',
8
+ 'mouse.wheel.right',
9
+ ]);
10
+ const MOUSE_BUTTONS = new Set([
11
+ 'left',
12
+ 'middle',
13
+ 'right',
14
+ 'wheel',
15
+ 'unknown',
16
+ ]);
17
+ const MODIFIERS = ['ctrl', 'alt', 'gui', 'shift'];
18
+ /** Validate untrusted JSON before passing it to Screen.dispatch(). */
19
+ export function isSystemEvent(value) {
20
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
21
+ return false;
22
+ }
23
+ const event = value;
24
+ switch (event.type) {
25
+ case 'resize':
26
+ case 'focus':
27
+ case 'blur':
28
+ return true;
29
+ case 'paste':
30
+ return typeof event.text === 'string';
31
+ case 'key':
32
+ return (MODIFIERS.every(mod => typeof event[mod] === 'boolean') &&
33
+ typeof event.char === 'string' &&
34
+ typeof event.name === 'string' &&
35
+ event.name.length > 0 &&
36
+ typeof event.full === 'string' &&
37
+ event.full.length > 0);
38
+ case 'mouse':
39
+ return (MODIFIERS.every(mod => typeof event[mod] === 'boolean') &&
40
+ MOUSE_NAMES.has(event.name) &&
41
+ MOUSE_BUTTONS.has(event.button) &&
42
+ Number.isSafeInteger(event.x) &&
43
+ Number.isSafeInteger(event.y));
44
+ default:
45
+ return false;
46
+ }
47
+ }
48
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../lib/validate.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,GAAG,IAAI,GAAG,CAAuB;IAChD,eAAe;IACf,mBAAmB;IACnB,iBAAiB;IACjB,gBAAgB;IAChB,kBAAkB;IAClB,kBAAkB;IAClB,mBAAmB;CACpB,CAAC,CAAA;AACF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAc;IACzC,MAAM;IACN,QAAQ;IACR,OAAO;IACP,OAAO;IACP,SAAS;CACV,CAAC,CAAA;AACF,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAU,CAAA;AAE1D,sEAAsE;AACtE,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,OAAO,KAAK,CAAA;IACd,CAAC;IACD,MAAM,KAAK,GAAG,KAAgC,CAAA;IAE9C,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,QAAQ,CAAC;QACd,KAAK,OAAO,CAAC;QACb,KAAK,MAAM;YACT,OAAO,IAAI,CAAA;QACb,KAAK,OAAO;YACV,OAAO,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAA;QACvC,KAAK,KAAK;YACR,OAAO,CACL,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC;gBACvD,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;gBAC9B,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;gBAC9B,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;gBACrB,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;gBAC9B,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CACtB,CAAA;QACH,KAAK,OAAO;YACV,OAAO,CACL,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC;gBACvD,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,IAA4B,CAAC;gBACnD,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,MAAqB,CAAC;gBAC9C,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;gBAC7B,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAC9B,CAAA;QACH;YACE,OAAO,KAAK,CAAA;IAChB,CAAC;AACH,CAAC"}
package/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ TeaUI
2
+ Copyright (c) 2023, Colin T.A. Gray
3
+ https://github.com/colinta/teaui
4
+
5
+ With code from multiple sources
6
+ see https://github.com/colinta/teaui/blob/master/packages/core/LICENSE
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in
16
+ all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @teaui/remote-control
2
+
3
+ Optional local WebSocket input and snapshot transport for TeaUI. Depends on the
4
+ public APIs of `@teaui/core` and `@teaui/result`; core does not depend on this package.
5
+
6
+ ```ts
7
+ import {RemoteControlServer} from '@teaui/remote-control'
8
+
9
+ const remote = new RemoteControlServer()
10
+ screen.addEventSource(remote)
11
+ const detachSnapshot = remote.setSnapshotProvider(() => screen.snapshot())
12
+ screen.onExit(() => {
13
+ detachSnapshot()
14
+ void remote.close()
15
+ })
16
+
17
+ const result = await remote.listen()
18
+ if (result.ok) {
19
+ // Publish result.value.url privately. It contains the connection secret.
20
+ } else {
21
+ // Report result.error through the application's logger.
22
+ }
23
+ ```
24
+
25
+ Send a `SystemEvent` JSON message for input, or
26
+ `{"type":"snapshot","format":"plain"}` to request a snapshot. Use `"ansi"` for
27
+ styled output. The provider always returns one ANSI string; remote control removes
28
+ ANSI when plain output is requested. Capture never triggers a render.
29
+ `remote.sendSnapshot(format, ansiSnapshot)` applies the same conversion and
30
+ broadcasts to current clients. Each broadcast is encoded once, with per-client
31
+ backpressure checks and no retained snapshot cache.
32
+
33
+ Screen owns event subscriptions. The application owns the server's lifetime.
34
+ Closing the server preserves registrations, and a later `listen()` starts a fresh
35
+ session with a new token. `listen()` returns
36
+ `Result<RemoteControlAddress, RemoteControlError>` and `close()` returns
37
+ `Result<void, RemoteControlError>` through their promises; neither rejects for
38
+ operation failures. Result types and helpers are exported by `@teaui/result`.
39
+
40
+ `RemoteControlError` is a plain discriminated union exported by this package, not
41
+ a JavaScript `Error` subclass. Switch on `error.type`: `invalid-port` carries
42
+ `port`; `startup-failed`, `server-failed`, `send-failed`, and `snapshot-failed`
43
+ carry an original `cause`; `dispatch-failed` and `cleanup-failed` carry `causes`.
44
+ `callback-failed` identifies `callback: 'listening' | 'error'` and its `cause`.
45
+ Cancellation is `startup-canceled`. Request-validation variants are
46
+ `invalid-json`, `invalid-event` (with a `reason`), `binary-message`, and
47
+ `snapshot-unavailable`. Every variant has a display `message`.
48
+
49
+ `onError()` receives the same union. Startup and cleanup failures are reported
50
+ both through their Result and `onError()`; choose one reporting path to avoid
51
+ duplicates. Cancellation is not an error notification. Request failures keep the
52
+ existing wire `{type: 'error', sequence, code, message}` shape; local causes and
53
+ other diagnostic details are never serialized into replies.
54
+
55
+ See [the remote-control guide](../../apps/docs/docs/remote-control.mdx) for lifecycle,
56
+ protocol, and security details. The source-checkout JSONL driver is
57
+ `examples/remote-control.mjs`.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@teaui/remote-control",
3
+ "description": "Opt-in local WebSocket input and snapshots for TeaUI applications",
4
+ "author": "Colin T.A. Gray <colinta@colinta.com>",
5
+ "version": "1.17.16",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/colinta/teaui"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./.dist/index.d.ts",
15
+ "import": "./.dist/index.js",
16
+ "default": "./.dist/index.js"
17
+ }
18
+ },
19
+ "main": ".dist/index.js",
20
+ "types": ".dist/index.d.ts",
21
+ "files": [
22
+ ".dist/"
23
+ ],
24
+ "engines": {
25
+ "node": ">= 18.12.0"
26
+ },
27
+ "dependencies": {
28
+ "ws": "^8.21.3",
29
+ "@teaui/core": "1.18.16",
30
+ "@teaui/result": "1.17.16"
31
+ },
32
+ "devDependencies": {
33
+ "@types/ws": "^8.18.1",
34
+ "@teaui/shared": "1.18.16"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "scripts": {
40
+ "clean": "rm -rf .dist/",
41
+ "_build": "pnpm clean && pnpm tsc",
42
+ "build": "node ../../shared/check.js",
43
+ "typecheck": "tsc -p tsconfig.check.json --noEmit",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest"
46
+ }
47
+ }