@proj-airi/server-sdk 0.7.2-beta.2 → 0.8.0-alpha.3

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.d.mts CHANGED
@@ -1,32 +1,36 @@
1
1
  import { WebSocketBaseEvent, WebSocketEvent, WebSocketEvents } from "@proj-airi/server-shared/types";
2
+ export * from "@proj-airi/server-shared/types";
2
3
 
3
4
  //#region src/client.d.ts
4
5
  interface ClientOptions<C = undefined> {
5
6
  url?: string;
6
7
  name: string;
7
- possibleEvents?: Array<(keyof WebSocketEvents<C>)>;
8
+ possibleEvents?: Array<keyof WebSocketEvents<C>>;
8
9
  token?: string;
9
10
  onError?: (error: unknown) => void;
10
11
  onClose?: () => void;
11
12
  autoConnect?: boolean;
12
13
  autoReconnect?: boolean;
14
+ maxReconnectAttempts?: number;
13
15
  }
14
16
  declare class Client<C = undefined> {
15
17
  private connected;
16
- private opts;
17
- private websocket;
18
- private eventListeners;
19
- private reconnectAttempts;
18
+ private connecting;
19
+ private websocket?;
20
20
  private shouldClose;
21
+ private readonly opts;
22
+ private readonly eventListeners;
21
23
  constructor(options: ClientOptions<C>);
22
- retryWithExponentialBackoff(fn: () => void | Promise<void>, attempts?: number, maxAttempts?: number): Promise<void>;
23
- tryReconnectWithExponentialBackoff(): Promise<void>;
24
+ private retryWithExponentialBackoff;
25
+ private tryReconnectWithExponentialBackoff;
24
26
  private _connect;
25
27
  connect(): Promise<void>;
26
28
  private tryAnnounce;
27
29
  private tryAuthenticate;
30
+ private readonly handleMessageBound;
28
31
  private handleMessage;
29
32
  onEvent<E extends keyof WebSocketEvents<C>>(event: E, callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>): void;
33
+ offEvent<E extends keyof WebSocketEvents<C>>(event: E, callback?: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void): void;
30
34
  send(data: WebSocketEvent<C>): void;
31
35
  sendRaw(data: string | ArrayBufferLike | ArrayBufferView): void;
32
36
  close(): void;
package/dist/index.mjs CHANGED
@@ -1,17 +1,23 @@
1
1
  import WebSocket from "crossws/websocket";
2
2
 
3
- //#region ../../node_modules/.pnpm/@moeru+std@0.1.0-beta.7/node_modules/@moeru/std/dist/sleep/index.js
3
+ //#region ../../node_modules/.pnpm/@moeru+std@0.1.0-beta.13/node_modules/@moeru/std/dist/sleep/index.js
4
4
  const sleep = async (delay) => new Promise((resolve) => setTimeout(resolve, delay));
5
5
 
6
6
  //#endregion
7
7
  //#region src/client.ts
8
+ var ReconnectingError = class extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "ReconnectingError";
12
+ }
13
+ };
8
14
  var Client = class {
9
15
  connected = false;
10
- opts;
16
+ connecting = false;
11
17
  websocket;
12
- eventListeners = /* @__PURE__ */ new Map();
13
- reconnectAttempts = 0;
14
18
  shouldClose = false;
19
+ opts;
20
+ eventListeners = /* @__PURE__ */ new Map();
15
21
  constructor(options) {
16
22
  this.opts = {
17
23
  url: "ws://localhost:6121/ws",
@@ -20,75 +26,65 @@ var Client = class {
20
26
  onClose: () => {},
21
27
  autoConnect: true,
22
28
  autoReconnect: true,
29
+ maxReconnectAttempts: -1,
23
30
  ...options
24
31
  };
25
- if (this.opts.autoConnect) try {
26
- this.connect();
27
- } catch (err) {
28
- console.error(err);
29
- }
32
+ this.onEvent("module:authenticated", async (event) => {
33
+ if (event.data.authenticated) this.tryAnnounce();
34
+ else await this.retryWithExponentialBackoff(() => this.tryAuthenticate());
35
+ });
36
+ if (this.opts.autoConnect) this.connect();
30
37
  }
31
- async retryWithExponentialBackoff(fn, attempts = 0, maxAttempts = -1) {
32
- if (maxAttempts !== -1 && attempts >= maxAttempts) {
33
- console.error(`Maximum retry attempts (${maxAttempts}) reached`);
34
- return;
35
- }
36
- try {
37
- await fn();
38
- } catch (err) {
39
- console.error("Encountered an error when retrying", err);
40
- await sleep(2 ** attempts * 1e3);
41
- await this.retryWithExponentialBackoff(fn, attempts + 1, maxAttempts);
38
+ async retryWithExponentialBackoff(fn) {
39
+ const { maxReconnectAttempts } = this.opts;
40
+ let attempts = 0;
41
+ while (true) {
42
+ if (maxReconnectAttempts !== -1 && attempts >= maxReconnectAttempts) {
43
+ console.error(`Maximum retry attempts (${maxReconnectAttempts}) reached`);
44
+ return;
45
+ }
46
+ try {
47
+ await fn();
48
+ return;
49
+ } catch (err) {
50
+ if (err instanceof ReconnectingError) return;
51
+ this.opts.onError?.(err);
52
+ await sleep(Math.min(2 ** attempts * 1e3, 3e4));
53
+ attempts++;
54
+ }
42
55
  }
43
56
  }
44
57
  async tryReconnectWithExponentialBackoff() {
45
- await this.retryWithExponentialBackoff(() => this._connect(), this.reconnectAttempts);
58
+ if (this.shouldClose) return;
59
+ await this.retryWithExponentialBackoff(() => this._connect());
46
60
  }
47
61
  _connect() {
62
+ if (this.shouldClose || this.connected) return Promise.resolve();
63
+ if (this.connecting) return Promise.reject(new ReconnectingError("Already connecting"));
48
64
  return new Promise((resolve, reject) => {
49
- if (this.shouldClose) {
50
- resolve();
51
- return;
52
- }
53
- if (this.connected) {
54
- resolve();
55
- return;
56
- }
57
- this.websocket = new WebSocket(this.opts.url);
58
- this.onEvent("module:authenticated", async (event) => {
59
- const auth = event.data.authenticated;
60
- if (!auth) this.retryWithExponentialBackoff(() => this.tryAuthenticate());
61
- else this.tryAnnounce();
62
- });
63
- this.websocket.onerror = (event) => {
64
- this.opts.onError?.(event);
65
- if ("error" in event && event.error instanceof Error) {
66
- if (event.error.message === "Received network error or non-101 status code.") {
67
- this.connected = false;
68
- if (!this.opts.autoReconnect) {
69
- this.opts.onError?.(event);
70
- this.opts.onClose?.();
71
- reject(event.error);
72
- return;
73
- }
74
- reject(event.error);
75
- }
76
- }
77
- };
78
- this.websocket.onclose = () => {
79
- this.opts.onClose?.();
65
+ this.connecting = true;
66
+ const ws = new WebSocket(this.opts.url);
67
+ this.websocket = ws;
68
+ ws.onmessage = this.handleMessageBound;
69
+ ws.onerror = (event) => {
70
+ this.connecting = false;
80
71
  this.connected = false;
81
- if (!this.opts.autoReconnect) this.opts.onClose?.();
82
- else this.tryReconnectWithExponentialBackoff();
72
+ this.opts.onError?.(event);
73
+ reject(event?.error ?? /* @__PURE__ */ new Error("WebSocket error"));
83
74
  };
84
- this.websocket.onmessage = (event) => {
85
- this.handleMessage(event);
75
+ ws.onclose = () => {
76
+ this.connecting = false;
77
+ if (this.connected) {
78
+ this.connected = false;
79
+ this.opts.onClose?.();
80
+ }
81
+ if (this.opts.autoReconnect && !this.shouldClose) this.tryReconnectWithExponentialBackoff();
86
82
  };
87
- this.websocket.onopen = () => {
88
- this.reconnectAttempts = 0;
83
+ ws.onopen = () => {
84
+ this.connecting = false;
85
+ this.connected = true;
89
86
  if (this.opts.token) this.tryAuthenticate();
90
87
  else this.tryAnnounce();
91
- this.connected = true;
92
88
  resolve();
93
89
  };
94
90
  });
@@ -108,35 +104,50 @@ var Client = class {
108
104
  tryAuthenticate() {
109
105
  if (this.opts.token) this.send({
110
106
  type: "module:authenticate",
111
- data: { token: this.opts.token || "" }
107
+ data: { token: this.opts.token }
112
108
  });
113
109
  }
110
+ handleMessageBound = (event) => {
111
+ this.handleMessage(event);
112
+ };
114
113
  async handleMessage(event) {
115
114
  try {
116
115
  const data = JSON.parse(event.data);
117
116
  const listeners = this.eventListeners.get(data.type);
118
- if (!listeners) return;
119
- for (const listener of listeners) await listener(data);
117
+ if (!listeners?.size) return;
118
+ const executions = [];
119
+ for (const listener of listeners) executions.push(Promise.resolve(listener(data)));
120
+ await Promise.allSettled(executions);
120
121
  } catch (err) {
121
122
  console.error("Failed to parse message:", err);
122
123
  this.opts.onError?.(err);
123
124
  }
124
125
  }
125
126
  onEvent(event, callback) {
126
- if (!this.eventListeners.get(event)) this.eventListeners.set(event, []);
127
+ let listeners = this.eventListeners.get(event);
128
+ if (!listeners) {
129
+ listeners = /* @__PURE__ */ new Set();
130
+ this.eventListeners.set(event, listeners);
131
+ }
132
+ listeners.add(callback);
133
+ }
134
+ offEvent(event, callback) {
127
135
  const listeners = this.eventListeners.get(event);
128
136
  if (!listeners) return;
129
- listeners.push(callback);
137
+ if (callback) {
138
+ listeners.delete(callback);
139
+ if (!listeners.size) this.eventListeners.delete(event);
140
+ } else this.eventListeners.delete(event);
130
141
  }
131
142
  send(data) {
132
- this.websocket?.send(JSON.stringify(data));
143
+ if (this.websocket && this.connected) this.websocket.send(JSON.stringify(data));
133
144
  }
134
145
  sendRaw(data) {
135
- this.websocket?.send(data);
146
+ if (this.websocket && this.connected) this.websocket.send(data);
136
147
  }
137
148
  close() {
138
149
  this.shouldClose = true;
139
- if (this.connected && this.websocket) {
150
+ if (this.websocket) {
140
151
  this.websocket.close();
141
152
  this.connected = false;
142
153
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["options: ClientOptions<C>","fn: () => void | Promise<void>","event: any","event: E","callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>","data: WebSocketEvent<C>","data: string | ArrayBufferLike | ArrayBufferView"],"sources":["../../../node_modules/.pnpm/@moeru+std@0.1.0-beta.7/node_modules/@moeru/std/dist/sleep/index.js","../src/client.ts"],"sourcesContent":["const sleep = async (delay) => new Promise((resolve) => setTimeout(resolve, delay));\n\nexport { sleep };\n","import type { WebSocketBaseEvent, WebSocketEvent, WebSocketEvents } from '@proj-airi/server-shared/types'\n\nimport WebSocket from 'crossws/websocket'\n\nimport { sleep } from '@moeru/std'\n\nexport interface ClientOptions<C = undefined> {\n url?: string\n name: string\n possibleEvents?: Array<(keyof WebSocketEvents<C>)>\n token?: string\n onError?: (error: unknown) => void\n onClose?: () => void\n autoConnect?: boolean\n autoReconnect?: boolean\n}\n\nexport class Client<C = undefined> {\n private connected = false\n private opts: Required<Omit<ClientOptions<C>, 'token'>> & Pick<ClientOptions<C>, 'token'>\n private websocket: WebSocket | undefined\n private eventListeners: Map<keyof WebSocketEvents<C>, Array<(data: WebSocketBaseEvent<any, any>) => void | Promise<void>>> = new Map()\n\n private reconnectAttempts = 0\n private shouldClose = false\n\n constructor(options: ClientOptions<C>) {\n this.opts = {\n url: 'ws://localhost:6121/ws',\n possibleEvents: [],\n onError: () => { },\n onClose: () => { },\n autoConnect: true,\n autoReconnect: true,\n ...options,\n }\n\n if (this.opts.autoConnect) {\n try {\n this.connect()\n }\n catch (err) {\n console.error(err)\n }\n }\n }\n\n async retryWithExponentialBackoff(fn: () => void | Promise<void>, attempts = 0, maxAttempts = -1) {\n if (maxAttempts !== -1 && attempts >= maxAttempts) {\n console.error(`Maximum retry attempts (${maxAttempts}) reached`)\n return\n }\n\n try {\n await fn()\n }\n catch (err) {\n console.error('Encountered an error when retrying', err)\n await sleep(2 ** attempts * 1000)\n await this.retryWithExponentialBackoff(fn, attempts + 1, maxAttempts)\n }\n }\n\n async tryReconnectWithExponentialBackoff() {\n await this.retryWithExponentialBackoff(() => this._connect(), this.reconnectAttempts)\n }\n\n private _connect() {\n return new Promise<void>((resolve, reject) => {\n if (this.shouldClose) {\n resolve()\n return\n }\n\n if (this.connected) {\n resolve()\n return\n }\n\n this.websocket = new WebSocket(this.opts.url)\n\n this.onEvent('module:authenticated', async (event) => {\n const auth = event.data.authenticated\n if (!auth) {\n this.retryWithExponentialBackoff(() => this.tryAuthenticate())\n }\n else {\n this.tryAnnounce()\n }\n })\n\n this.websocket.onerror = (event) => {\n this.opts.onError?.(event)\n\n if ('error' in event && event.error instanceof Error) {\n if (event.error.message === 'Received network error or non-101 status code.') {\n this.connected = false\n\n if (!this.opts.autoReconnect) {\n this.opts.onError?.(event)\n this.opts.onClose?.()\n reject(event.error)\n return\n }\n\n reject(event.error)\n }\n }\n }\n\n this.websocket.onclose = () => {\n this.opts.onClose?.()\n this.connected = false\n\n if (!this.opts.autoReconnect) {\n this.opts.onClose?.()\n }\n else {\n this.tryReconnectWithExponentialBackoff()\n }\n }\n\n this.websocket.onmessage = (event) => {\n this.handleMessage(event)\n }\n\n this.websocket.onopen = () => {\n this.reconnectAttempts = 0\n\n if (this.opts.token) {\n this.tryAuthenticate()\n }\n else {\n this.tryAnnounce()\n }\n\n this.connected = true\n\n resolve()\n }\n })\n }\n\n async connect() {\n await this.tryReconnectWithExponentialBackoff()\n }\n\n private tryAnnounce() {\n this.send({\n type: 'module:announce',\n data: {\n name: this.opts.name,\n possibleEvents: this.opts.possibleEvents,\n },\n })\n }\n\n private tryAuthenticate() {\n if (this.opts.token) {\n this.send({ type: 'module:authenticate', data: { token: this.opts.token || '' } })\n }\n }\n\n private async handleMessage(event: any) {\n try {\n const data = JSON.parse(event.data) as WebSocketEvent<C>\n const listeners = this.eventListeners.get(data.type)\n if (!listeners)\n return\n\n for (const listener of listeners)\n await listener(data)\n }\n catch (err) {\n console.error('Failed to parse message:', err)\n this.opts.onError?.(err)\n }\n }\n\n onEvent<E extends keyof WebSocketEvents<C>>(\n event: E,\n callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>,\n ): void {\n if (!this.eventListeners.get(event)) {\n this.eventListeners.set(event, [])\n }\n\n const listeners = this.eventListeners.get(event)\n if (!listeners) {\n return\n }\n\n listeners.push(callback as unknown as (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>)\n }\n\n send(data: WebSocketEvent<C>): void {\n this.websocket?.send(JSON.stringify(data))\n }\n\n sendRaw(data: string | ArrayBufferLike | ArrayBufferView): void {\n this.websocket?.send(data)\n }\n\n close(): void {\n this.shouldClose = true\n\n if (this.connected && this.websocket) {\n this.websocket.close()\n this.connected = false\n }\n }\n}\n"],"x_google_ignoreList":[0],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["executions: Promise<void>[]"],"sources":["../../../node_modules/.pnpm/@moeru+std@0.1.0-beta.13/node_modules/@moeru/std/dist/sleep/index.js","../src/client.ts"],"sourcesContent":["const sleep = async (delay) => new Promise((resolve) => setTimeout(resolve, delay));\n\nexport { sleep };\n","import type { WebSocketBaseEvent, WebSocketEvent, WebSocketEvents } from '@proj-airi/server-shared/types'\n\nimport WebSocket from 'crossws/websocket'\n\nimport { sleep } from '@moeru/std'\n\nclass ReconnectingError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ReconnectingError'\n }\n}\n\nexport interface ClientOptions<C = undefined> {\n url?: string\n name: string\n possibleEvents?: Array<keyof WebSocketEvents<C>>\n token?: string\n onError?: (error: unknown) => void\n onClose?: () => void\n autoConnect?: boolean\n autoReconnect?: boolean\n maxReconnectAttempts?: number\n}\n\nexport class Client<C = undefined> {\n private connected = false\n private connecting = false\n private websocket?: WebSocket\n private shouldClose = false\n\n private readonly opts: Required<Omit<ClientOptions<C>, 'token'>> & Pick<ClientOptions<C>, 'token'>\n private readonly eventListeners = new Map<\n keyof WebSocketEvents<C>,\n Set<(data: WebSocketBaseEvent<any, any>) => void | Promise<void>>\n >()\n\n constructor(options: ClientOptions<C>) {\n this.opts = {\n url: 'ws://localhost:6121/ws',\n possibleEvents: [],\n onError: () => {},\n onClose: () => {},\n autoConnect: true,\n autoReconnect: true,\n maxReconnectAttempts: -1,\n ...options,\n }\n\n // Authentication listener is registered once only\n this.onEvent('module:authenticated', async (event) => {\n if (event.data.authenticated) {\n this.tryAnnounce()\n }\n else {\n await this.retryWithExponentialBackoff(() => this.tryAuthenticate())\n }\n })\n\n if (this.opts.autoConnect) {\n void this.connect()\n }\n }\n\n private async retryWithExponentialBackoff(fn: () => void | Promise<void>) {\n const { maxReconnectAttempts } = this.opts\n let attempts = 0\n\n // Loop until attempts exceed maxReconnectAttempts, or unlimited if -1\n while (true) {\n if (maxReconnectAttempts !== -1 && attempts >= maxReconnectAttempts) {\n console.error(`Maximum retry attempts (${maxReconnectAttempts}) reached`)\n return\n }\n\n try {\n await fn()\n return\n }\n catch (err) {\n if (err instanceof ReconnectingError) {\n return\n }\n\n this.opts.onError?.(err)\n const delay = Math.min(2 ** attempts * 1000, 30_000) // capped exponential backoff\n await sleep(delay)\n attempts++\n }\n }\n }\n\n private async tryReconnectWithExponentialBackoff() {\n if (this.shouldClose) {\n return\n }\n\n await this.retryWithExponentialBackoff(() => this._connect())\n }\n\n private _connect(): Promise<void> {\n if (this.shouldClose || this.connected) {\n return Promise.resolve()\n }\n if (this.connecting) {\n return Promise.reject(new ReconnectingError('Already connecting'))\n }\n\n return new Promise((resolve, reject) => {\n this.connecting = true\n\n const ws = new WebSocket(this.opts.url)\n this.websocket = ws\n\n ws.onmessage = this.handleMessageBound\n ws.onerror = (event: any) => {\n this.connecting = false\n this.connected = false\n\n this.opts.onError?.(event)\n reject(event?.error ?? new Error('WebSocket error'))\n }\n ws.onclose = () => {\n this.connecting = false\n\n if (this.connected) {\n this.connected = false\n this.opts.onClose?.()\n }\n if (this.opts.autoReconnect && !this.shouldClose) {\n void this.tryReconnectWithExponentialBackoff()\n }\n }\n ws.onopen = () => {\n this.connecting = false\n this.connected = true\n\n if (this.opts.token)\n this.tryAuthenticate()\n else\n this.tryAnnounce()\n\n resolve()\n }\n })\n }\n\n async connect() {\n await this.tryReconnectWithExponentialBackoff()\n }\n\n private tryAnnounce() {\n this.send({\n type: 'module:announce',\n data: {\n name: this.opts.name,\n possibleEvents: this.opts.possibleEvents,\n },\n })\n }\n\n private tryAuthenticate() {\n if (this.opts.token) {\n this.send({\n type: 'module:authenticate',\n data: { token: this.opts.token },\n })\n }\n }\n\n // bound reference avoids new closure allocation on every connect\n private readonly handleMessageBound = (event: MessageEvent) => {\n void this.handleMessage(event)\n }\n\n private async handleMessage(event: MessageEvent) {\n try {\n const data = JSON.parse(event.data as string) as WebSocketEvent<C>\n const listeners = this.eventListeners.get(data.type)\n if (!listeners?.size) {\n return\n }\n\n // Execute all listeners concurrently\n const executions: Promise<void>[] = []\n for (const listener of listeners) {\n executions.push(Promise.resolve(listener(data as any)))\n }\n await Promise.allSettled(executions)\n }\n catch (err) {\n console.error('Failed to parse message:', err)\n this.opts.onError?.(err)\n }\n }\n\n onEvent<E extends keyof WebSocketEvents<C>>(\n event: E,\n callback: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void | Promise<void>,\n ): void {\n let listeners = this.eventListeners.get(event)\n if (!listeners) {\n listeners = new Set()\n this.eventListeners.set(event, listeners)\n }\n listeners.add(callback as any)\n }\n\n offEvent<E extends keyof WebSocketEvents<C>>(\n event: E,\n callback?: (data: WebSocketBaseEvent<E, WebSocketEvents<C>[E]>) => void,\n ): void {\n const listeners = this.eventListeners.get(event)\n if (!listeners) {\n return\n }\n\n if (callback) {\n listeners.delete(callback as any)\n if (!listeners.size) {\n this.eventListeners.delete(event)\n }\n }\n else {\n this.eventListeners.delete(event)\n }\n }\n\n send(data: WebSocketEvent<C>): void {\n if (this.websocket && this.connected) {\n this.websocket.send(JSON.stringify(data))\n }\n }\n\n sendRaw(data: string | ArrayBufferLike | ArrayBufferView): void {\n if (this.websocket && this.connected) {\n this.websocket.send(data)\n }\n }\n\n close(): void {\n this.shouldClose = true\n if (this.websocket) {\n this.websocket.close()\n this.connected = false\n }\n }\n}\n"],"x_google_ignoreList":[0],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/utils/node/process.ts"],"sourcesContent":["import process from 'node:process'\n\nlet running = true\n\nfunction killProcess() {\n running = false\n}\n\nprocess.on('SIGTERM', () => {\n killProcess()\n})\nprocess.on('SIGINT', () => {\n killProcess()\n})\nprocess.on('uncaughtException', (e) => {\n console.error(e)\n killProcess()\n})\n\nexport function runUntilSignal() {\n setTimeout(() => {\n if (running)\n runUntilSignal()\n }, 10)\n}\n"],"mappings":";;;AAEA,IAAI,UAAU;AAEd,SAAS,cAAc;CACrB,UAAU;AACX;AAED,QAAQ,GAAG,WAAW,MAAM;CAC1B,aAAa;AACd,EAAC;AACF,QAAQ,GAAG,UAAU,MAAM;CACzB,aAAa;AACd,EAAC;AACF,QAAQ,GAAG,qBAAqB,CAAC,MAAM;CACrC,QAAQ,MAAM,EAAE;CAChB,aAAa;AACd,EAAC;AAEF,SAAgB,iBAAiB;CAC/B,WAAW,MAAM;AACf,MAAI,SACF,gBAAgB;CACnB,GAAE,GAAG;AACP"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../src/utils/node/process.ts"],"sourcesContent":["import process from 'node:process'\n\nlet running = true\n\nfunction killProcess() {\n running = false\n}\n\nprocess.on('SIGTERM', () => {\n killProcess()\n})\nprocess.on('SIGINT', () => {\n killProcess()\n})\nprocess.on('uncaughtException', (e) => {\n console.error(e)\n killProcess()\n})\n\nexport function runUntilSignal() {\n setTimeout(() => {\n if (running)\n runUntilSignal()\n }, 10)\n}\n"],"mappings":";;;AAEA,IAAI,UAAU;AAEd,SAAS,cAAc;AACrB,WAAU;;AAGZ,QAAQ,GAAG,iBAAiB;AAC1B,cAAa;EACb;AACF,QAAQ,GAAG,gBAAgB;AACzB,cAAa;EACb;AACF,QAAQ,GAAG,sBAAsB,MAAM;AACrC,SAAQ,MAAM,EAAE;AAChB,cAAa;EACb;AAEF,SAAgB,iBAAiB;AAC/B,kBAAiB;AACf,MAAI,QACF,iBAAgB;IACjB,GAAG"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@proj-airi/server-sdk",
3
3
  "type": "module",
4
- "version": "0.7.2-beta.2",
4
+ "version": "0.8.0-alpha.3",
5
5
  "description": "Client-side SDK implementation for connecting to AIRI server components and runtimes",
6
6
  "author": {
7
7
  "name": "Moeru AI Project AIRI Team",
@@ -34,10 +34,10 @@
34
34
  "dependencies": {
35
35
  "crossws": "^0.4.1",
36
36
  "defu": "^6.1.4",
37
- "@proj-airi/server-shared": "^0.7.2-beta.2"
37
+ "@proj-airi/server-shared": "^0.8.0-alpha.3"
38
38
  },
39
39
  "devDependencies": {
40
- "@moeru/std": "0.1.0-beta.7"
40
+ "@moeru/std": "0.1.0-beta.13"
41
41
  },
42
42
  "scripts": {
43
43
  "dev": "pnpm run build",