@camera.ui/rpc 1.0.3 → 1.0.4
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/externals/nats.js/core/src/authenticator.ts +159 -0
- package/externals/nats.js/core/src/bench.ts +426 -0
- package/externals/nats.js/core/src/codec.ts +28 -0
- package/externals/nats.js/core/src/core.ts +1219 -0
- package/externals/nats.js/core/src/databuffer.ts +129 -0
- package/externals/nats.js/core/src/denobuffer.ts +248 -0
- package/externals/nats.js/core/src/encoders.ts +53 -0
- package/externals/nats.js/core/src/errors.ts +300 -0
- package/externals/nats.js/core/src/headers.ts +315 -0
- package/externals/nats.js/core/src/heartbeats.ts +114 -0
- package/externals/nats.js/core/src/idleheartbeat_monitor.ts +140 -0
- package/externals/nats.js/core/src/internal_mod.ts +167 -0
- package/externals/nats.js/core/src/ipparser.ts +215 -0
- package/externals/nats.js/core/src/mod.ts +113 -0
- package/externals/nats.js/core/src/msg.ts +120 -0
- package/externals/nats.js/core/src/muxsubscription.ts +111 -0
- package/externals/nats.js/core/src/nats.ts +650 -0
- package/externals/nats.js/core/src/nkeys.ts +1 -0
- package/externals/nats.js/core/src/nuid.ts +16 -0
- package/externals/nats.js/core/src/options.ts +202 -0
- package/externals/nats.js/core/src/parser.ts +756 -0
- package/externals/nats.js/core/src/protocol.ts +1304 -0
- package/externals/nats.js/core/src/queued_iterator.ts +171 -0
- package/externals/nats.js/core/src/request.ts +177 -0
- package/externals/nats.js/core/src/semver.ts +165 -0
- package/externals/nats.js/core/src/servers.ts +424 -0
- package/externals/nats.js/core/src/transport.ts +117 -0
- package/externals/nats.js/core/src/types.ts +17 -0
- package/externals/nats.js/core/src/util.ts +367 -0
- package/externals/nats.js/core/src/version.ts +2 -0
- package/externals/nats.js/core/src/ws_transport.ts +391 -0
- package/package.json +2 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020-2024 The NATS Authors
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
ConnectionOptions,
|
|
18
|
+
NatsConnection,
|
|
19
|
+
Server,
|
|
20
|
+
ServerInfo,
|
|
21
|
+
} from "./core.ts";
|
|
22
|
+
import type { Deferred } from "./util.ts";
|
|
23
|
+
import { deferred, delay, render } from "./util.ts";
|
|
24
|
+
import type { Transport, TransportFactory } from "./transport.ts";
|
|
25
|
+
import { extractProtocolMessage, setTransportFactory } from "./transport.ts";
|
|
26
|
+
import { checkOptions } from "./options.ts";
|
|
27
|
+
import { DataBuffer } from "./databuffer.ts";
|
|
28
|
+
import { INFO } from "./protocol.ts";
|
|
29
|
+
import { NatsConnectionImpl } from "./nats.ts";
|
|
30
|
+
import { version } from "./version.ts";
|
|
31
|
+
import { errors, InvalidArgumentError } from "./errors.ts";
|
|
32
|
+
|
|
33
|
+
const VERSION = version;
|
|
34
|
+
const LANG = "nats.ws";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* WsSocketFactory is a factory that returns a WebSocket and a boolean
|
|
38
|
+
* indicating if the connection is encrypted. Client code is responsible
|
|
39
|
+
* for creating a W3C WebSocket compliant transport.
|
|
40
|
+
*
|
|
41
|
+
* @param u the url to connect to
|
|
42
|
+
* @param opts the connection options
|
|
43
|
+
* @returns a promise that resolves to a WebSocket and a boolean indicating if
|
|
44
|
+
* the connection is encrypted
|
|
45
|
+
*/
|
|
46
|
+
export type WsSocketFactory = (u: string, opts: ConnectionOptions) => Promise<{
|
|
47
|
+
socket: WebSocket;
|
|
48
|
+
encrypted: boolean;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* WsConnectionOptions exposes wsconnect specific options not applicable to
|
|
53
|
+
* other transports.
|
|
54
|
+
*/
|
|
55
|
+
export interface WsConnectionOptions extends ConnectionOptions {
|
|
56
|
+
wsFactory?: WsSocketFactory;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class WsTransport implements Transport {
|
|
60
|
+
version: string;
|
|
61
|
+
lang: string;
|
|
62
|
+
closeError?: Error;
|
|
63
|
+
connected: boolean;
|
|
64
|
+
private done: boolean;
|
|
65
|
+
// @ts-ignore: expecting global WebSocket
|
|
66
|
+
private socket: WebSocket;
|
|
67
|
+
private options!: WsConnectionOptions;
|
|
68
|
+
socketClosed: boolean;
|
|
69
|
+
encrypted: boolean;
|
|
70
|
+
peeked: boolean;
|
|
71
|
+
|
|
72
|
+
yields: Uint8Array[];
|
|
73
|
+
signal: Deferred<void>;
|
|
74
|
+
closedNotification: Deferred<void | Error>;
|
|
75
|
+
|
|
76
|
+
constructor() {
|
|
77
|
+
this.version = VERSION;
|
|
78
|
+
this.lang = LANG;
|
|
79
|
+
this.connected = false;
|
|
80
|
+
this.done = false;
|
|
81
|
+
this.socketClosed = false;
|
|
82
|
+
this.encrypted = false;
|
|
83
|
+
this.peeked = false;
|
|
84
|
+
this.yields = [];
|
|
85
|
+
this.signal = deferred();
|
|
86
|
+
this.closedNotification = deferred();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async connect(
|
|
90
|
+
server: Server,
|
|
91
|
+
options: WsConnectionOptions,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
const connected = false;
|
|
94
|
+
const ok = deferred<void>();
|
|
95
|
+
|
|
96
|
+
this.options = options;
|
|
97
|
+
const u = server.src;
|
|
98
|
+
if (options.wsFactory) {
|
|
99
|
+
const { socket, encrypted } = await options.wsFactory(
|
|
100
|
+
server.src,
|
|
101
|
+
options,
|
|
102
|
+
);
|
|
103
|
+
this.socket = socket;
|
|
104
|
+
this.encrypted = encrypted;
|
|
105
|
+
} else {
|
|
106
|
+
this.encrypted = u.indexOf("wss://") === 0;
|
|
107
|
+
this.socket = new WebSocket(u);
|
|
108
|
+
}
|
|
109
|
+
this.socket.binaryType = "arraybuffer";
|
|
110
|
+
|
|
111
|
+
this.socket.onopen = () => {
|
|
112
|
+
if (this.done) {
|
|
113
|
+
this._closed(new Error("aborted"));
|
|
114
|
+
}
|
|
115
|
+
// we don't do anything here...
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
this.socket.onmessage = (me: MessageEvent) => {
|
|
119
|
+
if (this.done) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.yields.push(new Uint8Array(me.data));
|
|
123
|
+
if (this.peeked) {
|
|
124
|
+
this.signal.resolve();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const t = DataBuffer.concat(...this.yields);
|
|
128
|
+
const pm = extractProtocolMessage(t);
|
|
129
|
+
if (pm !== "") {
|
|
130
|
+
const m = INFO.exec(pm);
|
|
131
|
+
if (!m) {
|
|
132
|
+
if (options.debug) {
|
|
133
|
+
console.error("!!!", render(t));
|
|
134
|
+
}
|
|
135
|
+
ok.reject(new Error("unexpected response from server"));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const info = JSON.parse(m[1]) as ServerInfo;
|
|
140
|
+
checkOptions(info, this.options);
|
|
141
|
+
this.peeked = true;
|
|
142
|
+
this.connected = true;
|
|
143
|
+
this.signal.resolve();
|
|
144
|
+
ok.resolve();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
ok.reject(err);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// @ts-ignore: CloseEvent is provided in browsers
|
|
153
|
+
this.socket.onclose = (evt: CloseEvent) => {
|
|
154
|
+
let reason: Error | undefined;
|
|
155
|
+
// camera.ui fork patch — application close codes (4000-4999) carry
|
|
156
|
+
// semantic information from the server (e.g. 4401 = auth required).
|
|
157
|
+
// Surface them as Errors regardless of `wasClean`, since a server-
|
|
158
|
+
// initiated `close(code, reason)` post-upgrade lands as a clean close
|
|
159
|
+
// and the original `!evt.wasClean` gate would silently drop it.
|
|
160
|
+
if (evt.code >= 4000 && evt.code <= 4999) {
|
|
161
|
+
reason = new Error(evt.reason || `close code ${evt.code}`);
|
|
162
|
+
} else if (!evt.wasClean && evt.reason !== "") {
|
|
163
|
+
reason = new Error(evt.reason);
|
|
164
|
+
}
|
|
165
|
+
this._closed(reason);
|
|
166
|
+
this._cleanup();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// @ts-ignore: signature can be any
|
|
170
|
+
this.socket.onerror = (e: ErrorEvent | Event): void => {
|
|
171
|
+
if (this.done) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const evt = e as ErrorEvent;
|
|
175
|
+
const err = new errors.ConnectionError(evt.message);
|
|
176
|
+
if (!connected) {
|
|
177
|
+
ok.reject(err);
|
|
178
|
+
} else {
|
|
179
|
+
this._closed(err);
|
|
180
|
+
}
|
|
181
|
+
this._cleanup();
|
|
182
|
+
};
|
|
183
|
+
return ok;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
_cleanup() {
|
|
187
|
+
if (this.socketClosed === false) {
|
|
188
|
+
// node seems to not emit closed if there's an error
|
|
189
|
+
// all other runtimes do.
|
|
190
|
+
this.socketClosed = true;
|
|
191
|
+
this.socket.onopen = null;
|
|
192
|
+
this.socket.onmessage = null;
|
|
193
|
+
this.socket.onerror = null;
|
|
194
|
+
this.socket.onclose = null;
|
|
195
|
+
this.closedNotification.resolve(this.closeError);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
disconnect(): void {
|
|
200
|
+
this._closed(undefined, true);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async _closed(err?: Error, _internal = true): Promise<void> {
|
|
204
|
+
if (this.done) {
|
|
205
|
+
try {
|
|
206
|
+
this.socket.close();
|
|
207
|
+
} catch (_) {
|
|
208
|
+
// nothing
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.closeError = err;
|
|
213
|
+
if (!err) {
|
|
214
|
+
// camera.ui fork patch — bound the graceful-drain wait.
|
|
215
|
+
// A silent-dead TCP socket (5G handoff, carrier NAT-evict, etc.) keeps
|
|
216
|
+
// bufferedAmount > 0 forever because the OS never gets to flush the
|
|
217
|
+
// bytes and never reports the failure. Upstream waits indefinitely,
|
|
218
|
+
// which traps the protocol in `staleConnection` with no recovery path.
|
|
219
|
+
// After DRAIN_TIMEOUT_MS we accept the loss and force-close below.
|
|
220
|
+
const DRAIN_TIMEOUT_MS = 3000;
|
|
221
|
+
const drainStart = Date.now();
|
|
222
|
+
while (!this.socketClosed && this.socket.bufferedAmount > 0) {
|
|
223
|
+
if (Date.now() - drainStart >= DRAIN_TIMEOUT_MS) break;
|
|
224
|
+
await delay(100);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
this.done = true;
|
|
228
|
+
try {
|
|
229
|
+
this.socket.close();
|
|
230
|
+
} catch (_) {
|
|
231
|
+
// ignore this
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// camera.ui fork patch — guarantee `closedNotification` resolves even if
|
|
235
|
+
// the underlying WebSocket never fires `onclose`/`onerror` (zombie sockets
|
|
236
|
+
// observed in WKWebView on flaky cellular). _cleanup() is idempotent —
|
|
237
|
+
// if onclose does fire later, the second call is a no-op.
|
|
238
|
+
this._cleanup();
|
|
239
|
+
|
|
240
|
+
return this.closedNotification as Promise<void>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get isClosed(): boolean {
|
|
244
|
+
return this.done;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
[Symbol.asyncIterator]() {
|
|
248
|
+
return this.iterate();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async *iterate(): AsyncIterableIterator<Uint8Array> {
|
|
252
|
+
while (true) {
|
|
253
|
+
if (this.done) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (this.yields.length === 0) {
|
|
257
|
+
await this.signal;
|
|
258
|
+
}
|
|
259
|
+
const yields = this.yields;
|
|
260
|
+
this.yields = [];
|
|
261
|
+
for (let i = 0; i < yields.length; i++) {
|
|
262
|
+
if (this.options.debug) {
|
|
263
|
+
console.info(`> ${render(yields[i])}`);
|
|
264
|
+
}
|
|
265
|
+
yield yields[i];
|
|
266
|
+
}
|
|
267
|
+
// yielding could have paused and microtask
|
|
268
|
+
// could have added messages. Prevent allocations
|
|
269
|
+
// if possible
|
|
270
|
+
if (this.done) {
|
|
271
|
+
break;
|
|
272
|
+
} else if (this.yields.length === 0) {
|
|
273
|
+
yields.length = 0;
|
|
274
|
+
this.yields = yields;
|
|
275
|
+
this.signal = deferred();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
isEncrypted(): boolean {
|
|
281
|
+
return this.connected && this.encrypted;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
send(frame: Uint8Array): void {
|
|
285
|
+
if (this.done) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
this.socket.send(frame.buffer);
|
|
290
|
+
if (this.options.debug) {
|
|
291
|
+
console.info(`< ${render(frame)}`);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
// we ignore write errors because client will
|
|
296
|
+
// fail on a read or when the heartbeat timer
|
|
297
|
+
// detects a stale connection
|
|
298
|
+
if (this.options.debug) {
|
|
299
|
+
console.error(`!!! ${render(frame)}: ${err}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
close(err?: Error | undefined): Promise<void> {
|
|
305
|
+
return this._closed(err, false);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
closed(): Promise<void | Error> {
|
|
309
|
+
return this.closedNotification;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// this is to allow a force discard on a connection
|
|
313
|
+
// if the connection fails during the handshake protocol.
|
|
314
|
+
// Firefox for example, will keep connections going,
|
|
315
|
+
// so eventually if it succeeds, the client will have
|
|
316
|
+
// an additional transport running. With this
|
|
317
|
+
discard() {
|
|
318
|
+
this.socket?.close();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function wsUrlParseFn(u: string, encrypted?: boolean): string {
|
|
323
|
+
const ut = /^(.*:\/\/)(.*)/;
|
|
324
|
+
if (!ut.test(u)) {
|
|
325
|
+
// if we have no hint to encrypted and no protocol, assume encrypted
|
|
326
|
+
// else we fix the url from the update to match
|
|
327
|
+
if (typeof encrypted === "boolean") {
|
|
328
|
+
u = `${encrypted === true ? "https" : "http"}://${u}`;
|
|
329
|
+
} else {
|
|
330
|
+
u = `https://${u}`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
let url = new URL(u);
|
|
334
|
+
const srcProto = url.protocol.toLowerCase();
|
|
335
|
+
if (srcProto === "ws:") {
|
|
336
|
+
encrypted = false;
|
|
337
|
+
}
|
|
338
|
+
if (srcProto === "wss:") {
|
|
339
|
+
encrypted = true;
|
|
340
|
+
}
|
|
341
|
+
if (srcProto !== "https:" && srcProto !== "http") {
|
|
342
|
+
u = u.replace(/^(.*:\/\/)(.*)/gm, "$2");
|
|
343
|
+
url = new URL(`http://${u}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let protocol;
|
|
347
|
+
let port;
|
|
348
|
+
const host = url.hostname;
|
|
349
|
+
const path = url.pathname;
|
|
350
|
+
const search = url.search || "";
|
|
351
|
+
|
|
352
|
+
switch (srcProto) {
|
|
353
|
+
case "http:":
|
|
354
|
+
case "ws:":
|
|
355
|
+
case "nats:":
|
|
356
|
+
port = url.port || "80";
|
|
357
|
+
protocol = "ws:";
|
|
358
|
+
break;
|
|
359
|
+
case "https:":
|
|
360
|
+
case "wss:":
|
|
361
|
+
case "tls:":
|
|
362
|
+
port = url.port || "443";
|
|
363
|
+
protocol = "wss:";
|
|
364
|
+
break;
|
|
365
|
+
default:
|
|
366
|
+
port = url.port || encrypted === true ? "443" : "80";
|
|
367
|
+
protocol = encrypted === true ? "wss:" : "ws:";
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
return `${protocol}//${host}:${port}${path}${search}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function wsconnect(
|
|
374
|
+
opts: ConnectionOptions | WsConnectionOptions = {},
|
|
375
|
+
): Promise<NatsConnection> {
|
|
376
|
+
setTransportFactory({
|
|
377
|
+
defaultPort: 443,
|
|
378
|
+
urlParseFn: wsUrlParseFn,
|
|
379
|
+
factory: (): Transport => {
|
|
380
|
+
if (opts.tls) {
|
|
381
|
+
throw InvalidArgumentError.format(
|
|
382
|
+
"tls",
|
|
383
|
+
"is not configurable on w3c websocket connections",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return new WsTransport();
|
|
387
|
+
},
|
|
388
|
+
} as TransportFactory);
|
|
389
|
+
|
|
390
|
+
return NatsConnectionImpl.connect(opts);
|
|
391
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camera.ui/rpc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "camera.ui rpc package",
|
|
5
5
|
"author": "seydx (https://github.com/cameraui/rpc)",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"lint": "eslint .",
|
|
29
29
|
"lint:fix": "eslint --fix .",
|
|
30
30
|
"test": "vitest run",
|
|
31
|
+
"prepack": "test -f externals/nats.js/core/src/mod.ts || { echo 'ERROR: nats.js fork source missing. Run: git submodule update --init --recursive'; exit 1; }",
|
|
31
32
|
"prepublishOnly": "npm i --package-lock-only && npm run lint:fix && npm run format && npm run build"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|