@proj-airi/server-sdk 0.3.6 → 0.4.14

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/dist/index.cjs CHANGED
@@ -12,37 +12,110 @@ function sleep(ms) {
12
12
  }
13
13
 
14
14
  class Client {
15
+ connected = false;
15
16
  opts;
16
17
  websocket;
17
18
  eventListeners = /* @__PURE__ */ new Map();
18
- authenticateAttempts = 0;
19
+ reconnectAttempts = 0;
20
+ shouldClose = false;
19
21
  constructor(options) {
20
22
  this.opts = defu.defu(
21
23
  options,
22
- { url: "ws://localhost:6121/ws", possibleEvents: [] }
24
+ {
25
+ url: "ws://localhost:6121/ws",
26
+ possibleEvents: [],
27
+ onError: () => {
28
+ },
29
+ onClose: () => {
30
+ },
31
+ autoConnect: true,
32
+ autoReconnect: true
33
+ }
23
34
  );
24
- this.websocket = new WebSocket__default(this.opts.url);
25
- this.onEvent("module:authenticated", async (event) => {
26
- const auth = event.data.authenticated;
27
- if (!auth) {
28
- this.authenticateAttempts++;
29
- await sleep(2 ** this.authenticateAttempts * 1e3);
30
- this.tryAuthenticate();
31
- } else {
32
- this.tryAnnounce();
35
+ if (this.opts.autoConnect) {
36
+ try {
37
+ this.connect();
38
+ } catch (err) {
39
+ console.error(err);
33
40
  }
34
- });
35
- this.websocket.onmessage = this.handleMessage.bind(this);
36
- this.websocket.onopen = () => {
37
- if (this.opts.token) {
38
- this.tryAuthenticate();
39
- } else {
40
- this.tryAnnounce();
41
+ }
42
+ }
43
+ async retryWithExponentialBackoff(fn, attempts = 0, maxAttempts = -1) {
44
+ if (maxAttempts !== -1 && attempts >= maxAttempts) {
45
+ console.error(`Maximum retry attempts (${maxAttempts}) reached`);
46
+ return;
47
+ }
48
+ try {
49
+ await fn();
50
+ } catch (err) {
51
+ console.error("Encountered an error when retrying", err);
52
+ await sleep(2 ** attempts * 1e3);
53
+ await this.retryWithExponentialBackoff(fn, attempts++, maxAttempts);
54
+ }
55
+ }
56
+ async tryReconnectWithExponentialBackoff() {
57
+ await this.retryWithExponentialBackoff(() => this._connect(), this.reconnectAttempts);
58
+ }
59
+ _connect() {
60
+ return new Promise((resolve, reject) => {
61
+ if (this.shouldClose) {
62
+ resolve();
63
+ return;
41
64
  }
42
- };
43
- this.websocket.onclose = () => {
44
- this.authenticateAttempts = 0;
45
- };
65
+ if (this.connected) {
66
+ resolve();
67
+ return;
68
+ }
69
+ this.websocket = new WebSocket__default(this.opts.url);
70
+ this.onEvent("module:authenticated", async (event) => {
71
+ const auth = event.data.authenticated;
72
+ if (!auth) {
73
+ this.retryWithExponentialBackoff(() => this.tryAuthenticate());
74
+ } else {
75
+ this.tryAnnounce();
76
+ }
77
+ });
78
+ this.websocket.onerror = (event) => {
79
+ this.opts.onError?.(event);
80
+ if ("error" in event && event.error instanceof Error) {
81
+ if (event.error.message === "Received network error or non-101 status code.") {
82
+ this.connected = false;
83
+ if (!this.opts.autoReconnect) {
84
+ this.opts.onError?.(event);
85
+ this.opts.onClose?.();
86
+ reject(event.error);
87
+ return;
88
+ }
89
+ reject(event.error);
90
+ }
91
+ }
92
+ };
93
+ this.websocket.onclose = () => {
94
+ this.opts.onClose?.();
95
+ this.connected = false;
96
+ if (!this.opts.autoReconnect) {
97
+ this.opts.onClose?.();
98
+ } else {
99
+ this.tryReconnectWithExponentialBackoff();
100
+ }
101
+ };
102
+ this.websocket.onmessage = (event) => {
103
+ this.handleMessage(event);
104
+ };
105
+ this.websocket.onopen = () => {
106
+ this.reconnectAttempts = 0;
107
+ if (this.opts.token) {
108
+ this.tryAuthenticate();
109
+ } else {
110
+ this.tryAnnounce();
111
+ }
112
+ this.connected = true;
113
+ resolve();
114
+ };
115
+ });
116
+ }
117
+ async connect() {
118
+ await this.tryReconnectWithExponentialBackoff();
46
119
  }
47
120
  tryAnnounce() {
48
121
  this.send({
@@ -59,26 +132,41 @@ class Client {
59
132
  }
60
133
  }
61
134
  async handleMessage(event) {
62
- const data = JSON.parse(event.data);
63
- const listeners = this.eventListeners.get(data.type);
64
- if (!listeners)
65
- return;
66
- for (const listener of listeners)
67
- await listener(data);
135
+ try {
136
+ const data = JSON.parse(event.data);
137
+ const listeners = this.eventListeners.get(data.type);
138
+ if (!listeners)
139
+ return;
140
+ for (const listener of listeners)
141
+ await listener(data);
142
+ } catch (err) {
143
+ console.error("Failed to parse message:", err);
144
+ this.opts.onError?.(err);
145
+ }
68
146
  }
69
147
  onEvent(event, callback) {
70
148
  if (!this.eventListeners.get(event)) {
71
149
  this.eventListeners.set(event, []);
72
150
  }
73
151
  const listeners = this.eventListeners.get(event);
152
+ if (!listeners) {
153
+ return;
154
+ }
74
155
  listeners.push(callback);
75
156
  this.eventListeners.set(event, listeners);
76
157
  }
77
158
  send(data) {
78
- this.websocket.send(JSON.stringify(data));
159
+ this.websocket?.send(JSON.stringify(data));
79
160
  }
80
161
  sendRaw(data) {
81
- this.websocket.send(data);
162
+ this.websocket?.send(data);
163
+ }
164
+ close() {
165
+ this.shouldClose = true;
166
+ if (this.connected && this.websocket) {
167
+ this.websocket.close();
168
+ this.connected = false;
169
+ }
82
170
  }
83
171
  }
84
172
 
package/dist/index.d.cts CHANGED
@@ -1,24 +1,34 @@
1
1
  import { WebSocketEvents, WebSocketBaseEvent, WebSocketEvent } from '@proj-airi/server-shared/types';
2
- import { Blob } from 'node:buffer';
3
2
 
4
- interface ClientOptions {
3
+ interface ClientOptions<C = undefined> {
5
4
  url?: string;
6
5
  name: string;
7
- possibleEvents?: Array<(keyof WebSocketEvents)>;
6
+ possibleEvents?: Array<(keyof WebSocketEvents<C>)>;
8
7
  token?: string;
8
+ onError?: (error: unknown) => void;
9
+ onClose?: () => void;
10
+ autoConnect?: boolean;
11
+ autoReconnect?: boolean;
9
12
  }
10
- declare class Client {
13
+ declare class Client<C = undefined> {
14
+ private connected;
11
15
  private opts;
12
16
  private websocket;
13
17
  private eventListeners;
14
- private authenticateAttempts;
15
- constructor(options: ClientOptions);
18
+ private reconnectAttempts;
19
+ private shouldClose;
20
+ constructor(options: ClientOptions<C>);
21
+ retryWithExponentialBackoff(fn: () => void | Promise<void>, attempts?: number, maxAttempts?: number): Promise<void>;
22
+ tryReconnectWithExponentialBackoff(): Promise<void>;
23
+ private _connect;
24
+ connect(): Promise<void>;
16
25
  private tryAnnounce;
17
26
  private tryAuthenticate;
18
27
  private handleMessage;
19
- onEvent<E extends keyof WebSocketEvents>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents[E]>) => void | Promise<void>): void;
20
- send(data: WebSocketEvent): void;
21
- sendRaw(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
28
+ onEvent<E extends keyof WebSocketEvents<C>>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>): void;
29
+ send(data: WebSocketEvent<C>): void;
30
+ sendRaw(data: string | ArrayBufferLike | ArrayBufferView): void;
31
+ close(): void;
22
32
  }
23
33
 
24
34
  export { Client, type ClientOptions };
package/dist/index.d.mts CHANGED
@@ -1,24 +1,34 @@
1
1
  import { WebSocketEvents, WebSocketBaseEvent, WebSocketEvent } from '@proj-airi/server-shared/types';
2
- import { Blob } from 'node:buffer';
3
2
 
4
- interface ClientOptions {
3
+ interface ClientOptions<C = undefined> {
5
4
  url?: string;
6
5
  name: string;
7
- possibleEvents?: Array<(keyof WebSocketEvents)>;
6
+ possibleEvents?: Array<(keyof WebSocketEvents<C>)>;
8
7
  token?: string;
8
+ onError?: (error: unknown) => void;
9
+ onClose?: () => void;
10
+ autoConnect?: boolean;
11
+ autoReconnect?: boolean;
9
12
  }
10
- declare class Client {
13
+ declare class Client<C = undefined> {
14
+ private connected;
11
15
  private opts;
12
16
  private websocket;
13
17
  private eventListeners;
14
- private authenticateAttempts;
15
- constructor(options: ClientOptions);
18
+ private reconnectAttempts;
19
+ private shouldClose;
20
+ constructor(options: ClientOptions<C>);
21
+ retryWithExponentialBackoff(fn: () => void | Promise<void>, attempts?: number, maxAttempts?: number): Promise<void>;
22
+ tryReconnectWithExponentialBackoff(): Promise<void>;
23
+ private _connect;
24
+ connect(): Promise<void>;
16
25
  private tryAnnounce;
17
26
  private tryAuthenticate;
18
27
  private handleMessage;
19
- onEvent<E extends keyof WebSocketEvents>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents[E]>) => void | Promise<void>): void;
20
- send(data: WebSocketEvent): void;
21
- sendRaw(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
28
+ onEvent<E extends keyof WebSocketEvents<C>>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>): void;
29
+ send(data: WebSocketEvent<C>): void;
30
+ sendRaw(data: string | ArrayBufferLike | ArrayBufferView): void;
31
+ close(): void;
22
32
  }
23
33
 
24
34
  export { Client, type ClientOptions };
package/dist/index.d.ts CHANGED
@@ -1,24 +1,34 @@
1
1
  import { WebSocketEvents, WebSocketBaseEvent, WebSocketEvent } from '@proj-airi/server-shared/types';
2
- import { Blob } from 'node:buffer';
3
2
 
4
- interface ClientOptions {
3
+ interface ClientOptions<C = undefined> {
5
4
  url?: string;
6
5
  name: string;
7
- possibleEvents?: Array<(keyof WebSocketEvents)>;
6
+ possibleEvents?: Array<(keyof WebSocketEvents<C>)>;
8
7
  token?: string;
8
+ onError?: (error: unknown) => void;
9
+ onClose?: () => void;
10
+ autoConnect?: boolean;
11
+ autoReconnect?: boolean;
9
12
  }
10
- declare class Client {
13
+ declare class Client<C = undefined> {
14
+ private connected;
11
15
  private opts;
12
16
  private websocket;
13
17
  private eventListeners;
14
- private authenticateAttempts;
15
- constructor(options: ClientOptions);
18
+ private reconnectAttempts;
19
+ private shouldClose;
20
+ constructor(options: ClientOptions<C>);
21
+ retryWithExponentialBackoff(fn: () => void | Promise<void>, attempts?: number, maxAttempts?: number): Promise<void>;
22
+ tryReconnectWithExponentialBackoff(): Promise<void>;
23
+ private _connect;
24
+ connect(): Promise<void>;
16
25
  private tryAnnounce;
17
26
  private tryAuthenticate;
18
27
  private handleMessage;
19
- onEvent<E extends keyof WebSocketEvents>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents[E]>) => void | Promise<void>): void;
20
- send(data: WebSocketEvent): void;
21
- sendRaw(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
28
+ onEvent<E extends keyof WebSocketEvents<C>>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>): void;
29
+ send(data: WebSocketEvent<C>): void;
30
+ sendRaw(data: string | ArrayBufferLike | ArrayBufferView): void;
31
+ close(): void;
22
32
  }
23
33
 
24
34
  export { Client, type ClientOptions };
package/dist/index.mjs CHANGED
@@ -6,37 +6,110 @@ function sleep(ms) {
6
6
  }
7
7
 
8
8
  class Client {
9
+ connected = false;
9
10
  opts;
10
11
  websocket;
11
12
  eventListeners = /* @__PURE__ */ new Map();
12
- authenticateAttempts = 0;
13
+ reconnectAttempts = 0;
14
+ shouldClose = false;
13
15
  constructor(options) {
14
16
  this.opts = defu(
15
17
  options,
16
- { url: "ws://localhost:6121/ws", possibleEvents: [] }
18
+ {
19
+ url: "ws://localhost:6121/ws",
20
+ possibleEvents: [],
21
+ onError: () => {
22
+ },
23
+ onClose: () => {
24
+ },
25
+ autoConnect: true,
26
+ autoReconnect: true
27
+ }
17
28
  );
18
- this.websocket = new WebSocket(this.opts.url);
19
- this.onEvent("module:authenticated", async (event) => {
20
- const auth = event.data.authenticated;
21
- if (!auth) {
22
- this.authenticateAttempts++;
23
- await sleep(2 ** this.authenticateAttempts * 1e3);
24
- this.tryAuthenticate();
25
- } else {
26
- this.tryAnnounce();
29
+ if (this.opts.autoConnect) {
30
+ try {
31
+ this.connect();
32
+ } catch (err) {
33
+ console.error(err);
27
34
  }
28
- });
29
- this.websocket.onmessage = this.handleMessage.bind(this);
30
- this.websocket.onopen = () => {
31
- if (this.opts.token) {
32
- this.tryAuthenticate();
33
- } else {
34
- this.tryAnnounce();
35
+ }
36
+ }
37
+ async retryWithExponentialBackoff(fn, attempts = 0, maxAttempts = -1) {
38
+ if (maxAttempts !== -1 && attempts >= maxAttempts) {
39
+ console.error(`Maximum retry attempts (${maxAttempts}) reached`);
40
+ return;
41
+ }
42
+ try {
43
+ await fn();
44
+ } catch (err) {
45
+ console.error("Encountered an error when retrying", err);
46
+ await sleep(2 ** attempts * 1e3);
47
+ await this.retryWithExponentialBackoff(fn, attempts++, maxAttempts);
48
+ }
49
+ }
50
+ async tryReconnectWithExponentialBackoff() {
51
+ await this.retryWithExponentialBackoff(() => this._connect(), this.reconnectAttempts);
52
+ }
53
+ _connect() {
54
+ return new Promise((resolve, reject) => {
55
+ if (this.shouldClose) {
56
+ resolve();
57
+ return;
35
58
  }
36
- };
37
- this.websocket.onclose = () => {
38
- this.authenticateAttempts = 0;
39
- };
59
+ if (this.connected) {
60
+ resolve();
61
+ return;
62
+ }
63
+ this.websocket = new WebSocket(this.opts.url);
64
+ this.onEvent("module:authenticated", async (event) => {
65
+ const auth = event.data.authenticated;
66
+ if (!auth) {
67
+ this.retryWithExponentialBackoff(() => this.tryAuthenticate());
68
+ } else {
69
+ this.tryAnnounce();
70
+ }
71
+ });
72
+ this.websocket.onerror = (event) => {
73
+ this.opts.onError?.(event);
74
+ if ("error" in event && event.error instanceof Error) {
75
+ if (event.error.message === "Received network error or non-101 status code.") {
76
+ this.connected = false;
77
+ if (!this.opts.autoReconnect) {
78
+ this.opts.onError?.(event);
79
+ this.opts.onClose?.();
80
+ reject(event.error);
81
+ return;
82
+ }
83
+ reject(event.error);
84
+ }
85
+ }
86
+ };
87
+ this.websocket.onclose = () => {
88
+ this.opts.onClose?.();
89
+ this.connected = false;
90
+ if (!this.opts.autoReconnect) {
91
+ this.opts.onClose?.();
92
+ } else {
93
+ this.tryReconnectWithExponentialBackoff();
94
+ }
95
+ };
96
+ this.websocket.onmessage = (event) => {
97
+ this.handleMessage(event);
98
+ };
99
+ this.websocket.onopen = () => {
100
+ this.reconnectAttempts = 0;
101
+ if (this.opts.token) {
102
+ this.tryAuthenticate();
103
+ } else {
104
+ this.tryAnnounce();
105
+ }
106
+ this.connected = true;
107
+ resolve();
108
+ };
109
+ });
110
+ }
111
+ async connect() {
112
+ await this.tryReconnectWithExponentialBackoff();
40
113
  }
41
114
  tryAnnounce() {
42
115
  this.send({
@@ -53,26 +126,41 @@ class Client {
53
126
  }
54
127
  }
55
128
  async handleMessage(event) {
56
- const data = JSON.parse(event.data);
57
- const listeners = this.eventListeners.get(data.type);
58
- if (!listeners)
59
- return;
60
- for (const listener of listeners)
61
- await listener(data);
129
+ try {
130
+ const data = JSON.parse(event.data);
131
+ const listeners = this.eventListeners.get(data.type);
132
+ if (!listeners)
133
+ return;
134
+ for (const listener of listeners)
135
+ await listener(data);
136
+ } catch (err) {
137
+ console.error("Failed to parse message:", err);
138
+ this.opts.onError?.(err);
139
+ }
62
140
  }
63
141
  onEvent(event, callback) {
64
142
  if (!this.eventListeners.get(event)) {
65
143
  this.eventListeners.set(event, []);
66
144
  }
67
145
  const listeners = this.eventListeners.get(event);
146
+ if (!listeners) {
147
+ return;
148
+ }
68
149
  listeners.push(callback);
69
150
  this.eventListeners.set(event, listeners);
70
151
  }
71
152
  send(data) {
72
- this.websocket.send(JSON.stringify(data));
153
+ this.websocket?.send(JSON.stringify(data));
73
154
  }
74
155
  sendRaw(data) {
75
- this.websocket.send(data);
156
+ this.websocket?.send(data);
157
+ }
158
+ close() {
159
+ this.shouldClose = true;
160
+ if (this.connected && this.websocket) {
161
+ this.websocket.close();
162
+ this.connected = false;
163
+ }
76
164
  }
77
165
  }
78
166
 
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const process = require('node:process');
4
+
5
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
6
+
7
+ const process__default = /*#__PURE__*/_interopDefaultCompat(process);
8
+
9
+ let running = true;
10
+ function killProcess() {
11
+ running = false;
12
+ }
13
+ process__default.on("SIGTERM", () => {
14
+ killProcess();
15
+ });
16
+ process__default.on("SIGINT", () => {
17
+ killProcess();
18
+ });
19
+ process__default.on("uncaughtException", (e) => {
20
+ console.error(e);
21
+ killProcess();
22
+ });
23
+ function runUntilSignal() {
24
+ setTimeout(() => {
25
+ if (running)
26
+ runUntilSignal();
27
+ }, 10);
28
+ }
29
+
30
+ exports.runUntilSignal = runUntilSignal;
@@ -0,0 +1,3 @@
1
+ declare function runUntilSignal(): void;
2
+
3
+ export { runUntilSignal };
@@ -0,0 +1,3 @@
1
+ declare function runUntilSignal(): void;
2
+
3
+ export { runUntilSignal };
@@ -0,0 +1,3 @@
1
+ declare function runUntilSignal(): void;
2
+
3
+ export { runUntilSignal };
@@ -0,0 +1,24 @@
1
+ import process from 'node:process';
2
+
3
+ let running = true;
4
+ function killProcess() {
5
+ running = false;
6
+ }
7
+ process.on("SIGTERM", () => {
8
+ killProcess();
9
+ });
10
+ process.on("SIGINT", () => {
11
+ killProcess();
12
+ });
13
+ process.on("uncaughtException", (e) => {
14
+ console.error(e);
15
+ killProcess();
16
+ });
17
+ function runUntilSignal() {
18
+ setTimeout(() => {
19
+ if (running)
20
+ runUntilSignal();
21
+ }, 10);
22
+ }
23
+
24
+ export { runUntilSignal };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@proj-airi/server-sdk",
3
3
  "type": "module",
4
- "version": "0.3.6",
4
+ "version": "0.4.14",
5
5
  "description": "Client-side SDK implementation for connecting to Airi server components and runtimes",
6
6
  "author": {
7
7
  "name": "Neko Ayaka",
@@ -19,6 +19,11 @@
19
19
  "types": "./dist/index.d.ts",
20
20
  "import": "./dist/index.mjs",
21
21
  "require": "./dist/index.cjs"
22
+ },
23
+ "./utils/node": {
24
+ "types": "./dist/utils/node/index.d.ts",
25
+ "import": "./dist/utils/node/index.mjs",
26
+ "require": "./dist/utils/node/index.cjs"
22
27
  }
23
28
  },
24
29
  "main": "./dist/index.cjs",
@@ -32,7 +37,7 @@
32
37
  "dependencies": {
33
38
  "crossws": "^0.3.4",
34
39
  "defu": "^6.1.4",
35
- "@proj-airi/server-shared": "^0.3.6"
40
+ "@proj-airi/server-shared": "^0.4.14"
36
41
  },
37
42
  "scripts": {
38
43
  "dev": "pnpm run stub",