@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.
Files changed (32) hide show
  1. package/externals/nats.js/core/src/authenticator.ts +159 -0
  2. package/externals/nats.js/core/src/bench.ts +426 -0
  3. package/externals/nats.js/core/src/codec.ts +28 -0
  4. package/externals/nats.js/core/src/core.ts +1219 -0
  5. package/externals/nats.js/core/src/databuffer.ts +129 -0
  6. package/externals/nats.js/core/src/denobuffer.ts +248 -0
  7. package/externals/nats.js/core/src/encoders.ts +53 -0
  8. package/externals/nats.js/core/src/errors.ts +300 -0
  9. package/externals/nats.js/core/src/headers.ts +315 -0
  10. package/externals/nats.js/core/src/heartbeats.ts +114 -0
  11. package/externals/nats.js/core/src/idleheartbeat_monitor.ts +140 -0
  12. package/externals/nats.js/core/src/internal_mod.ts +167 -0
  13. package/externals/nats.js/core/src/ipparser.ts +215 -0
  14. package/externals/nats.js/core/src/mod.ts +113 -0
  15. package/externals/nats.js/core/src/msg.ts +120 -0
  16. package/externals/nats.js/core/src/muxsubscription.ts +111 -0
  17. package/externals/nats.js/core/src/nats.ts +650 -0
  18. package/externals/nats.js/core/src/nkeys.ts +1 -0
  19. package/externals/nats.js/core/src/nuid.ts +16 -0
  20. package/externals/nats.js/core/src/options.ts +202 -0
  21. package/externals/nats.js/core/src/parser.ts +756 -0
  22. package/externals/nats.js/core/src/protocol.ts +1304 -0
  23. package/externals/nats.js/core/src/queued_iterator.ts +171 -0
  24. package/externals/nats.js/core/src/request.ts +177 -0
  25. package/externals/nats.js/core/src/semver.ts +165 -0
  26. package/externals/nats.js/core/src/servers.ts +424 -0
  27. package/externals/nats.js/core/src/transport.ts +117 -0
  28. package/externals/nats.js/core/src/types.ts +17 -0
  29. package/externals/nats.js/core/src/util.ts +367 -0
  30. package/externals/nats.js/core/src/version.ts +2 -0
  31. package/externals/nats.js/core/src/ws_transport.ts +391 -0
  32. 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",
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": {