@nemnesia/symbol-websocket 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # 変更履歴
2
+
3
+ このプロジェクトにおけるすべての重要な変更は、このファイルに記録されます。
4
+
5
+ 変更履歴のフォーマットは[変更履歴の管理](https://keepachangelog.com/en/1.0.0/)に基づいています。
6
+
7
+ ## [0.1.0] - 2025/12/29
8
+
9
+ ### 追加
10
+
11
+ - Symbol ブロックチェーンのWebSocket接続をサポート。
12
+ - リアルタイムデータ取得機能(ブロック、トランザクション、アカウント情報など)。
13
+ - 柔軟なサブスクリプション管理機能。
14
+ - 自動再接続機能とサブスクリプション復元機能。
15
+ - エラーおよびクローズイベントのハンドリング機能。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ccHarvestasya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Symbol WebSocket
2
+
3
+ Symbol WebSocket は、Symbol ブロックチェーンのリアルタイムデータを監視するための TypeScript ライブラリです。このライブラリは、WebSocket を使用してブロックチェーンデータを効率的に取得し、サブスクリプションベースのイベントリスニングを提供します。
4
+
5
+ ## 特徴
6
+
7
+ - **リアルタイムデータ取得**: ブロック、トランザクション、アカウント情報などをリアルタイムで取得可能。
8
+ - **柔軟なサブスクリプション管理**: 必要なチャネルに簡単にサブスクライブおよびアンサブスクライブ可能。
9
+ - **エラーおよびクローズイベントのハンドリング**: WebSocket のエラーや接続終了を簡単に処理可能。
10
+ - **自動再接続**: 接続が切断された場合、自動的に再接続し、サブスクリプションを復元。
11
+
12
+ ## インストール
13
+
14
+ ```bash
15
+ npm install @nemnesia/symbol-websocket
16
+ ```
17
+
18
+ ## 使い方
19
+
20
+ ```typescript
21
+ import { SymbolWebSocket } from '@nemnesia/symbol-websocket';
22
+
23
+ const ws = new SymbolWebSocket({
24
+ host: 'localhost',
25
+ ssl: true,
26
+ timeout: 5000,
27
+ });
28
+
29
+ // チャネルにサブスクライブ
30
+ ws.on('confirmedAdded', (message) => {
31
+ console.log('New confirmed transaction:', message);
32
+ });
33
+
34
+ // エラーイベントの登録
35
+ ws.onError((err) => {
36
+ console.error('WebSocket error:', err);
37
+ });
38
+
39
+ // クローズイベントの登録
40
+ ws.onClose((event) => {
41
+ console.log('WebSocket closed:', event);
42
+ });
43
+
44
+ // 切断
45
+ ws.disconnect();
46
+ ```
47
+
48
+ ## API
49
+
50
+ #### コンストラクタ
51
+
52
+ ```typescript
53
+ new SymbolWebSocket(options: SymbolWebSocketOptions);
54
+ ```
55
+
56
+ - `options`: 接続設定。
57
+ - `host`: 接続先ホスト。
58
+ - `ssl`: SSL を使用するかどうか。
59
+ - `timeout`: 接続タイムアウト(ミリ秒)。指定時間内に接続が完了しない場合はエラーになります。
60
+ - `autoReconnect`: 自動再接続を有効にするか(デフォルト: `true`)。
61
+ - `maxReconnectAttempts`: 最大再接続試行回数(デフォルト: `Infinity`)。
62
+ - `reconnectInterval`: 再接続の間隔(ミリ秒、デフォルト: `3000`)。
63
+
64
+ #### メソッド
65
+
66
+ - `on(channel: SymbolChannel, callback: (message: WebSocket.MessageEvent) => void): void`
67
+ - 指定したチャネルにサブスクライブします。
68
+ - `on(channel: SymbolChannel, address: string, callback: (message: WebSocket.MessageEvent) => void): void`
69
+ - アドレスを指定してチャネルにサブスクライブします。
70
+ - `off(channel: SymbolChannel): void`
71
+ - 指定したチャネルのサブスクリプションを解除します。
72
+ - `off(channel: SymbolChannel, address: string): void`
73
+ - アドレスを指定してチャネルのサブスクリプションを解除します。
74
+ - `onConnect(callback: (uid: string) => void): void`
75
+ - WebSocket 接続完了時のコールバックを登録します。
76
+ - `onReconnect(callback: (attemptCount: number) => void): void`
77
+ - 再接続試行時のコールバックを登録します。
78
+ - `onError(callback: (err: WebSocket.ErrorEvent) => void): void`
79
+ - エラーイベントのコールバックを登録します。
80
+ - `onClose(callback: (event: WebSocket.CloseEvent) => void): void`
81
+ - クローズイベントのコールバックを登録します。
82
+ - `disconnect(): void`
83
+ - WebSocket 接続を切断します。
84
+
85
+ ## 注意点
86
+
87
+ - 再接続は自動的に行われます(デフォルト有効)。
88
+ - 再接続時は既存のサブスクリプションが自動的に復元されます。
89
+ - `autoReconnect: false`を設定することで自動再接続を無効化できます。
90
+
91
+ ## ライセンス
92
+
93
+ このプロジェクトは [MITライセンス](./LICENSE) のもとで公開されています。
94
+
95
+ ## 貢献方法
96
+
97
+ バグ報告・機能要望・プルリクエストは [GitHubリポジトリ](https://github.com/nemnesia/symbol-tools/tree/main/packages/symbol-websocket) で受け付けています。お気軽にご参加ください。
98
+
99
+ ## バグ報告・質問
100
+
101
+ 問題や質問は [GitHub Issues](https://github.com/nemnesia/symbol-tools/issues) からご連絡ください。
package/dist/index.cjs ADDED
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SymbolWebSocket: () => SymbolWebSocket,
34
+ symbolChannelPaths: () => symbolChannelPaths
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/symbolChannelPaths.ts
39
+ var symbolChannelPaths = {
40
+ block: { subscribe: () => "block" },
41
+ finalizedBlock: { subscribe: () => "finalizedBlock" },
42
+ confirmedAdded: { subscribe: (address) => address ? `confirmedAdded/${address}` : "confirmedAdded" },
43
+ unconfirmedAdded: { subscribe: (address) => address ? `unconfirmedAdded/${address}` : "unconfirmedAdded" },
44
+ unconfirmedRemoved: {
45
+ subscribe: (address) => address ? `unconfirmedRemoved/${address}` : "unconfirmedRemoved"
46
+ },
47
+ partialAdded: { subscribe: (address) => address ? `partialAdded/${address}` : "partialAdded" },
48
+ partialRemoved: { subscribe: (address) => address ? `partialRemoved/${address}` : "partialRemoved" },
49
+ cosignature: { subscribe: (address) => address ? `cosignature/${address}` : "cosignature" },
50
+ status: { subscribe: (address) => address ? `status/${address}` : "status" }
51
+ };
52
+
53
+ // src/SymbolWebSocket.ts
54
+ var import_isomorphic_ws = __toESM(require("isomorphic-ws"), 1);
55
+ var WS_OPEN = import_isomorphic_ws.default.OPEN ?? 1;
56
+ var WS_CONNECTING = import_isomorphic_ws.default.CONNECTING ?? 0;
57
+ var SymbolWebSocket = class {
58
+ _client;
59
+ _uid = null;
60
+ isFirstMessage = true;
61
+ eventCallbacks = {};
62
+ pendingSubscribes = [];
63
+ errorCallbacks = [];
64
+ onCloseCallback = () => {
65
+ };
66
+ connectCallbacks = [];
67
+ reconnectCallbacks = [];
68
+ // 再接続関連のプロパティ
69
+ options;
70
+ reconnectAttempts = 0;
71
+ reconnectTimer = null;
72
+ connectionTimeoutTimer = null;
73
+ isManualDisconnect = false;
74
+ activeSubscriptions = /* @__PURE__ */ new Set();
75
+ /**
76
+ * コンストラクタ
77
+ * @param options Symbolウェブソケットオプション
78
+ */
79
+ constructor(options) {
80
+ this.options = {
81
+ autoReconnect: true,
82
+ maxReconnectAttempts: Infinity,
83
+ reconnectInterval: 3e3,
84
+ ...options
85
+ };
86
+ this.createConnection();
87
+ }
88
+ /**
89
+ * WebSocket接続を作成
90
+ */
91
+ createConnection() {
92
+ const endPointHost = this.options.host;
93
+ const ssl = this.options.ssl ?? false;
94
+ const protocol = ssl ? "wss" : "ws";
95
+ const endPointPort = ssl ? "3001" : "3000";
96
+ this._client = new import_isomorphic_ws.default(`${protocol}://${endPointHost}:${endPointPort}/ws`);
97
+ if (this.options.timeout) {
98
+ this.connectionTimeoutTimer = setTimeout(() => {
99
+ if (this._client.readyState === WS_CONNECTING || !this._uid) {
100
+ const timeoutError = new Error(`WebSocket connection timeout after ${this.options.timeout}ms`);
101
+ const errorEvent = { error: timeoutError, message: timeoutError.message };
102
+ this.errorCallbacks.forEach((cb) => cb(errorEvent));
103
+ this._client.close();
104
+ }
105
+ }, this.options.timeout);
106
+ }
107
+ this._client.onclose = (event) => {
108
+ this.onCloseCallback(event);
109
+ if (!this.isManualDisconnect && this.options.autoReconnect) {
110
+ this.attemptReconnect();
111
+ }
112
+ };
113
+ this._client.onerror = (err) => {
114
+ this.errorCallbacks.forEach((cb) => cb(err));
115
+ };
116
+ this._client.onmessage = (message) => {
117
+ try {
118
+ const data = JSON.parse(message.data.toString());
119
+ if (this.isFirstMessage) {
120
+ if (data.uid) {
121
+ this._uid = data.uid;
122
+ this.reconnectAttempts = 0;
123
+ if (this.connectionTimeoutTimer) {
124
+ clearTimeout(this.connectionTimeoutTimer);
125
+ this.connectionTimeoutTimer = null;
126
+ }
127
+ this.connectCallbacks.forEach((cb) => cb(this._uid));
128
+ this.activeSubscriptions.forEach((subscribePath) => {
129
+ this._client.send(JSON.stringify({ uid: this._uid, subscribe: subscribePath }));
130
+ });
131
+ this.pendingSubscribes.forEach(({ subscribePath }) => {
132
+ this._client.send(JSON.stringify({ uid: this._uid, subscribe: subscribePath }));
133
+ this.activeSubscriptions.add(subscribePath);
134
+ });
135
+ this.pendingSubscribes = [];
136
+ }
137
+ this.isFirstMessage = false;
138
+ return;
139
+ }
140
+ const channel = data.topic;
141
+ if (channel && this.eventCallbacks[channel]) {
142
+ this.eventCallbacks[channel].forEach((cb) => cb(data));
143
+ }
144
+ } catch (e) {
145
+ if (this.errorCallbacks.length > 0) {
146
+ const errorEvent = { ...e instanceof Error ? e : { message: String(e) } };
147
+ this.errorCallbacks.forEach((cb) => cb(errorEvent));
148
+ } else {
149
+ throw e;
150
+ }
151
+ }
152
+ };
153
+ }
154
+ /**
155
+ * 再接続を試みる
156
+ */
157
+ attemptReconnect() {
158
+ const maxAttempts = this.options.maxReconnectAttempts ?? Infinity;
159
+ if (this.reconnectAttempts >= maxAttempts) {
160
+ return;
161
+ }
162
+ this.reconnectAttempts++;
163
+ this.reconnectCallbacks.forEach((cb) => cb(this.reconnectAttempts));
164
+ const interval = this.options.reconnectInterval ?? 3e3;
165
+ this.reconnectTimer = setTimeout(() => {
166
+ this.isFirstMessage = true;
167
+ this._uid = null;
168
+ this.createConnection();
169
+ }, interval);
170
+ }
171
+ /**
172
+ * WebSocket接続完了イベント登録
173
+ * @param callback 接続時に呼ばれるコールバック
174
+ */
175
+ onConnect(callback) {
176
+ this.connectCallbacks.push(callback);
177
+ if (this._uid) {
178
+ callback(this._uid);
179
+ }
180
+ }
181
+ /**
182
+ * WebSocket再接続イベント登録
183
+ * @param callback 再接続試行時に呼ばれるコールバック
184
+ */
185
+ onReconnect(callback) {
186
+ this.reconnectCallbacks.push(callback);
187
+ }
188
+ /**
189
+ * UID
190
+ */
191
+ get uid() {
192
+ return this._uid;
193
+ }
194
+ /**
195
+ * クライアントインスタンスを取得
196
+ */
197
+ get client() {
198
+ return this._client;
199
+ }
200
+ /**
201
+ * 接続状態を取得
202
+ */
203
+ get isConnected() {
204
+ return this._client.readyState === WS_OPEN;
205
+ }
206
+ /**
207
+ * WebSocketエラーイベント登録
208
+ * @param callback エラー時に呼ばれるコールバック
209
+ */
210
+ onError(callback) {
211
+ this.errorCallbacks.push(callback);
212
+ }
213
+ /**
214
+ * WebSocketクローズイベント登録
215
+ * @param callback クローズ時に呼ばれるコールバック
216
+ */
217
+ onClose(callback) {
218
+ this.onCloseCallback = callback;
219
+ }
220
+ on(channel, addressOrCallback, callback) {
221
+ const address = typeof addressOrCallback === "string" ? addressOrCallback : void 0;
222
+ const actualCallback = typeof addressOrCallback === "function" ? addressOrCallback : callback;
223
+ const channelPath = symbolChannelPaths[channel];
224
+ const subscribePath = typeof channelPath.subscribe === "function" ? channelPath.subscribe(address) : channelPath.subscribe;
225
+ if (!subscribePath) {
226
+ throw new Error(`Subscribe path could not be determined for channel: ${channel}`);
227
+ }
228
+ if (!this.eventCallbacks[subscribePath]) {
229
+ this.eventCallbacks[subscribePath] = [];
230
+ }
231
+ this.eventCallbacks[subscribePath].push(actualCallback);
232
+ if (!this._uid) {
233
+ this.pendingSubscribes.push({ subscribePath, callback: actualCallback });
234
+ return;
235
+ }
236
+ if (this._client.readyState === WS_OPEN) {
237
+ this._client.send(JSON.stringify({ uid: this._uid, subscribe: subscribePath }));
238
+ this.activeSubscriptions.add(subscribePath);
239
+ }
240
+ }
241
+ off(channel, address) {
242
+ const channelPath = symbolChannelPaths[channel];
243
+ const subscribePath = typeof channelPath.subscribe === "function" ? channelPath.subscribe(address) : channelPath.subscribe;
244
+ if (!subscribePath) {
245
+ throw new Error(`Subscribe path could not be determined for channel: ${channel}`);
246
+ }
247
+ delete this.eventCallbacks[subscribePath];
248
+ this.activeSubscriptions.delete(subscribePath);
249
+ if (this._uid && this._client.readyState === WS_OPEN) {
250
+ this._client.send(JSON.stringify({ uid: this._uid, unsubscribe: subscribePath }));
251
+ }
252
+ }
253
+ /**
254
+ * WebSocket接続を切断
255
+ */
256
+ disconnect() {
257
+ this.isManualDisconnect = true;
258
+ if (this.reconnectTimer) {
259
+ clearTimeout(this.reconnectTimer);
260
+ this.reconnectTimer = null;
261
+ }
262
+ if (this.connectionTimeoutTimer) {
263
+ clearTimeout(this.connectionTimeoutTimer);
264
+ this.connectionTimeoutTimer = null;
265
+ }
266
+ this.eventCallbacks = {};
267
+ this.pendingSubscribes = [];
268
+ this.errorCallbacks = [];
269
+ this.connectCallbacks = [];
270
+ this.reconnectCallbacks = [];
271
+ this.activeSubscriptions.clear();
272
+ if (this._client.readyState === WS_OPEN || this._client.readyState === WS_CONNECTING) {
273
+ this._client.close();
274
+ }
275
+ this._uid = null;
276
+ this.isFirstMessage = true;
277
+ this.reconnectAttempts = 0;
278
+ }
279
+ };
280
+ // Annotate the CommonJS export names for ESM import in node:
281
+ 0 && (module.exports = {
282
+ SymbolWebSocket,
283
+ symbolChannelPaths
284
+ });
@@ -0,0 +1,144 @@
1
+ import WebSocket from 'isomorphic-ws';
2
+
3
+ /**
4
+ * Symbolチャネルタイプ
5
+ * - block: 生成されたブロックの通知
6
+ * - finalizedBlock: ファイナライズ通知
7
+ * - confirmedAdded: 承認トランザクション通知
8
+ * - unconfirmedAdded: 未承認トランザクション通知
9
+ * - unconfirmedRemoved: 未承認トランザクション削除通知
10
+ * - partialAdded: パーシャル追加通知
11
+ * - partialRemoved: パーシャル削除通知
12
+ * - cosignature: 連署要求通知
13
+ * - status: ステータス通知
14
+ */
15
+ type SymbolChannel = 'block' | 'finalizedBlock' | 'confirmedAdded' | 'unconfirmedAdded' | 'unconfirmedRemoved' | 'partialAdded' | 'partialRemoved' | 'cosignature' | 'status';
16
+ /**
17
+ * Symbolチャネルパス定義
18
+ */
19
+ declare const symbolChannelPaths: Record<SymbolChannel, {
20
+ subscribe: (address?: string) => string;
21
+ }>;
22
+
23
+ /**
24
+ * Symbolウェブソケットモニターオプション
25
+ */
26
+ interface SymbolWebSocketOptions {
27
+ host: string;
28
+ /**
29
+ * 接続タイムアウト(ミリ秒)
30
+ */
31
+ timeout?: number;
32
+ ssl?: boolean;
33
+ /**
34
+ * 自動再接続を有効にする
35
+ * @default true
36
+ */
37
+ autoReconnect?: boolean;
38
+ /**
39
+ * 最大再接続試行回数
40
+ * @default Infinity
41
+ */
42
+ maxReconnectAttempts?: number;
43
+ /**
44
+ * 再接続間隔(ミリ秒)
45
+ * @default 3000
46
+ */
47
+ reconnectInterval?: number;
48
+ }
49
+
50
+ /**
51
+ * Symbolウェブソケットクラス
52
+ */
53
+ declare class SymbolWebSocket {
54
+ private _client;
55
+ private _uid;
56
+ private isFirstMessage;
57
+ private eventCallbacks;
58
+ private pendingSubscribes;
59
+ private errorCallbacks;
60
+ private onCloseCallback;
61
+ private connectCallbacks;
62
+ private reconnectCallbacks;
63
+ private options;
64
+ private reconnectAttempts;
65
+ private reconnectTimer;
66
+ private connectionTimeoutTimer;
67
+ private isManualDisconnect;
68
+ private activeSubscriptions;
69
+ /**
70
+ * コンストラクタ
71
+ * @param options Symbolウェブソケットオプション
72
+ */
73
+ constructor(options: SymbolWebSocketOptions);
74
+ /**
75
+ * WebSocket接続を作成
76
+ */
77
+ private createConnection;
78
+ /**
79
+ * 再接続を試みる
80
+ */
81
+ private attemptReconnect;
82
+ /**
83
+ * WebSocket接続完了イベント登録
84
+ * @param callback 接続時に呼ばれるコールバック
85
+ */
86
+ onConnect(callback: (uid: string) => void): void;
87
+ /**
88
+ * WebSocket再接続イベント登録
89
+ * @param callback 再接続試行時に呼ばれるコールバック
90
+ */
91
+ onReconnect(callback: (attemptCount: number) => void): void;
92
+ /**
93
+ * UID
94
+ */
95
+ get uid(): string | null;
96
+ /**
97
+ * クライアントインスタンスを取得
98
+ */
99
+ get client(): WebSocket;
100
+ /**
101
+ * 接続状態を取得
102
+ */
103
+ get isConnected(): boolean;
104
+ /**
105
+ * WebSocketエラーイベント登録
106
+ * @param callback エラー時に呼ばれるコールバック
107
+ */
108
+ onError(callback: (err: WebSocket.ErrorEvent) => void): void;
109
+ /**
110
+ * WebSocketクローズイベント登録
111
+ * @param callback クローズ時に呼ばれるコールバック
112
+ */
113
+ onClose(callback: (event: WebSocket.CloseEvent) => void): void;
114
+ /**
115
+ * チャネルサブスクメソッド
116
+ * @param channel チャネル名
117
+ * @param callback コールバック関数
118
+ */
119
+ on(channel: SymbolChannel, callback: (message: WebSocket.MessageEvent) => void): void;
120
+ /**
121
+ * チャネルサブスクメソッド
122
+ * @param channel チャネル名
123
+ * @param address アドレス
124
+ * @param callback コールバック関数
125
+ */
126
+ on(channel: SymbolChannel, address: string, callback: (message: WebSocket.MessageEvent) => void): void;
127
+ /**
128
+ * チャネルアンサブスクメソッド
129
+ * @param channel チャネル名
130
+ */
131
+ off(channel: SymbolChannel): void;
132
+ /**
133
+ * チャネルアンサブスクメソッド
134
+ * @param channel チャネル名
135
+ * @param address アドレス
136
+ */
137
+ off(channel: SymbolChannel, address: string): void;
138
+ /**
139
+ * WebSocket接続を切断
140
+ */
141
+ disconnect(): void;
142
+ }
143
+
144
+ export { type SymbolChannel, SymbolWebSocket, type SymbolWebSocketOptions, symbolChannelPaths };
@@ -0,0 +1,144 @@
1
+ import WebSocket from 'isomorphic-ws';
2
+
3
+ /**
4
+ * Symbolチャネルタイプ
5
+ * - block: 生成されたブロックの通知
6
+ * - finalizedBlock: ファイナライズ通知
7
+ * - confirmedAdded: 承認トランザクション通知
8
+ * - unconfirmedAdded: 未承認トランザクション通知
9
+ * - unconfirmedRemoved: 未承認トランザクション削除通知
10
+ * - partialAdded: パーシャル追加通知
11
+ * - partialRemoved: パーシャル削除通知
12
+ * - cosignature: 連署要求通知
13
+ * - status: ステータス通知
14
+ */
15
+ type SymbolChannel = 'block' | 'finalizedBlock' | 'confirmedAdded' | 'unconfirmedAdded' | 'unconfirmedRemoved' | 'partialAdded' | 'partialRemoved' | 'cosignature' | 'status';
16
+ /**
17
+ * Symbolチャネルパス定義
18
+ */
19
+ declare const symbolChannelPaths: Record<SymbolChannel, {
20
+ subscribe: (address?: string) => string;
21
+ }>;
22
+
23
+ /**
24
+ * Symbolウェブソケットモニターオプション
25
+ */
26
+ interface SymbolWebSocketOptions {
27
+ host: string;
28
+ /**
29
+ * 接続タイムアウト(ミリ秒)
30
+ */
31
+ timeout?: number;
32
+ ssl?: boolean;
33
+ /**
34
+ * 自動再接続を有効にする
35
+ * @default true
36
+ */
37
+ autoReconnect?: boolean;
38
+ /**
39
+ * 最大再接続試行回数
40
+ * @default Infinity
41
+ */
42
+ maxReconnectAttempts?: number;
43
+ /**
44
+ * 再接続間隔(ミリ秒)
45
+ * @default 3000
46
+ */
47
+ reconnectInterval?: number;
48
+ }
49
+
50
+ /**
51
+ * Symbolウェブソケットクラス
52
+ */
53
+ declare class SymbolWebSocket {
54
+ private _client;
55
+ private _uid;
56
+ private isFirstMessage;
57
+ private eventCallbacks;
58
+ private pendingSubscribes;
59
+ private errorCallbacks;
60
+ private onCloseCallback;
61
+ private connectCallbacks;
62
+ private reconnectCallbacks;
63
+ private options;
64
+ private reconnectAttempts;
65
+ private reconnectTimer;
66
+ private connectionTimeoutTimer;
67
+ private isManualDisconnect;
68
+ private activeSubscriptions;
69
+ /**
70
+ * コンストラクタ
71
+ * @param options Symbolウェブソケットオプション
72
+ */
73
+ constructor(options: SymbolWebSocketOptions);
74
+ /**
75
+ * WebSocket接続を作成
76
+ */
77
+ private createConnection;
78
+ /**
79
+ * 再接続を試みる
80
+ */
81
+ private attemptReconnect;
82
+ /**
83
+ * WebSocket接続完了イベント登録
84
+ * @param callback 接続時に呼ばれるコールバック
85
+ */
86
+ onConnect(callback: (uid: string) => void): void;
87
+ /**
88
+ * WebSocket再接続イベント登録
89
+ * @param callback 再接続試行時に呼ばれるコールバック
90
+ */
91
+ onReconnect(callback: (attemptCount: number) => void): void;
92
+ /**
93
+ * UID
94
+ */
95
+ get uid(): string | null;
96
+ /**
97
+ * クライアントインスタンスを取得
98
+ */
99
+ get client(): WebSocket;
100
+ /**
101
+ * 接続状態を取得
102
+ */
103
+ get isConnected(): boolean;
104
+ /**
105
+ * WebSocketエラーイベント登録
106
+ * @param callback エラー時に呼ばれるコールバック
107
+ */
108
+ onError(callback: (err: WebSocket.ErrorEvent) => void): void;
109
+ /**
110
+ * WebSocketクローズイベント登録
111
+ * @param callback クローズ時に呼ばれるコールバック
112
+ */
113
+ onClose(callback: (event: WebSocket.CloseEvent) => void): void;
114
+ /**
115
+ * チャネルサブスクメソッド
116
+ * @param channel チャネル名
117
+ * @param callback コールバック関数
118
+ */
119
+ on(channel: SymbolChannel, callback: (message: WebSocket.MessageEvent) => void): void;
120
+ /**
121
+ * チャネルサブスクメソッド
122
+ * @param channel チャネル名
123
+ * @param address アドレス
124
+ * @param callback コールバック関数
125
+ */
126
+ on(channel: SymbolChannel, address: string, callback: (message: WebSocket.MessageEvent) => void): void;
127
+ /**
128
+ * チャネルアンサブスクメソッド
129
+ * @param channel チャネル名
130
+ */
131
+ off(channel: SymbolChannel): void;
132
+ /**
133
+ * チャネルアンサブスクメソッド
134
+ * @param channel チャネル名
135
+ * @param address アドレス
136
+ */
137
+ off(channel: SymbolChannel, address: string): void;
138
+ /**
139
+ * WebSocket接続を切断
140
+ */
141
+ disconnect(): void;
142
+ }
143
+
144
+ export { type SymbolChannel, SymbolWebSocket, type SymbolWebSocketOptions, symbolChannelPaths };
package/dist/index.js ADDED
@@ -0,0 +1,246 @@
1
+ // src/symbolChannelPaths.ts
2
+ var symbolChannelPaths = {
3
+ block: { subscribe: () => "block" },
4
+ finalizedBlock: { subscribe: () => "finalizedBlock" },
5
+ confirmedAdded: { subscribe: (address) => address ? `confirmedAdded/${address}` : "confirmedAdded" },
6
+ unconfirmedAdded: { subscribe: (address) => address ? `unconfirmedAdded/${address}` : "unconfirmedAdded" },
7
+ unconfirmedRemoved: {
8
+ subscribe: (address) => address ? `unconfirmedRemoved/${address}` : "unconfirmedRemoved"
9
+ },
10
+ partialAdded: { subscribe: (address) => address ? `partialAdded/${address}` : "partialAdded" },
11
+ partialRemoved: { subscribe: (address) => address ? `partialRemoved/${address}` : "partialRemoved" },
12
+ cosignature: { subscribe: (address) => address ? `cosignature/${address}` : "cosignature" },
13
+ status: { subscribe: (address) => address ? `status/${address}` : "status" }
14
+ };
15
+
16
+ // src/SymbolWebSocket.ts
17
+ import WebSocket from "isomorphic-ws";
18
+ var WS_OPEN = WebSocket.OPEN ?? 1;
19
+ var WS_CONNECTING = WebSocket.CONNECTING ?? 0;
20
+ var SymbolWebSocket = class {
21
+ _client;
22
+ _uid = null;
23
+ isFirstMessage = true;
24
+ eventCallbacks = {};
25
+ pendingSubscribes = [];
26
+ errorCallbacks = [];
27
+ onCloseCallback = () => {
28
+ };
29
+ connectCallbacks = [];
30
+ reconnectCallbacks = [];
31
+ // 再接続関連のプロパティ
32
+ options;
33
+ reconnectAttempts = 0;
34
+ reconnectTimer = null;
35
+ connectionTimeoutTimer = null;
36
+ isManualDisconnect = false;
37
+ activeSubscriptions = /* @__PURE__ */ new Set();
38
+ /**
39
+ * コンストラクタ
40
+ * @param options Symbolウェブソケットオプション
41
+ */
42
+ constructor(options) {
43
+ this.options = {
44
+ autoReconnect: true,
45
+ maxReconnectAttempts: Infinity,
46
+ reconnectInterval: 3e3,
47
+ ...options
48
+ };
49
+ this.createConnection();
50
+ }
51
+ /**
52
+ * WebSocket接続を作成
53
+ */
54
+ createConnection() {
55
+ const endPointHost = this.options.host;
56
+ const ssl = this.options.ssl ?? false;
57
+ const protocol = ssl ? "wss" : "ws";
58
+ const endPointPort = ssl ? "3001" : "3000";
59
+ this._client = new WebSocket(`${protocol}://${endPointHost}:${endPointPort}/ws`);
60
+ if (this.options.timeout) {
61
+ this.connectionTimeoutTimer = setTimeout(() => {
62
+ if (this._client.readyState === WS_CONNECTING || !this._uid) {
63
+ const timeoutError = new Error(`WebSocket connection timeout after ${this.options.timeout}ms`);
64
+ const errorEvent = { error: timeoutError, message: timeoutError.message };
65
+ this.errorCallbacks.forEach((cb) => cb(errorEvent));
66
+ this._client.close();
67
+ }
68
+ }, this.options.timeout);
69
+ }
70
+ this._client.onclose = (event) => {
71
+ this.onCloseCallback(event);
72
+ if (!this.isManualDisconnect && this.options.autoReconnect) {
73
+ this.attemptReconnect();
74
+ }
75
+ };
76
+ this._client.onerror = (err) => {
77
+ this.errorCallbacks.forEach((cb) => cb(err));
78
+ };
79
+ this._client.onmessage = (message) => {
80
+ try {
81
+ const data = JSON.parse(message.data.toString());
82
+ if (this.isFirstMessage) {
83
+ if (data.uid) {
84
+ this._uid = data.uid;
85
+ this.reconnectAttempts = 0;
86
+ if (this.connectionTimeoutTimer) {
87
+ clearTimeout(this.connectionTimeoutTimer);
88
+ this.connectionTimeoutTimer = null;
89
+ }
90
+ this.connectCallbacks.forEach((cb) => cb(this._uid));
91
+ this.activeSubscriptions.forEach((subscribePath) => {
92
+ this._client.send(JSON.stringify({ uid: this._uid, subscribe: subscribePath }));
93
+ });
94
+ this.pendingSubscribes.forEach(({ subscribePath }) => {
95
+ this._client.send(JSON.stringify({ uid: this._uid, subscribe: subscribePath }));
96
+ this.activeSubscriptions.add(subscribePath);
97
+ });
98
+ this.pendingSubscribes = [];
99
+ }
100
+ this.isFirstMessage = false;
101
+ return;
102
+ }
103
+ const channel = data.topic;
104
+ if (channel && this.eventCallbacks[channel]) {
105
+ this.eventCallbacks[channel].forEach((cb) => cb(data));
106
+ }
107
+ } catch (e) {
108
+ if (this.errorCallbacks.length > 0) {
109
+ const errorEvent = { ...e instanceof Error ? e : { message: String(e) } };
110
+ this.errorCallbacks.forEach((cb) => cb(errorEvent));
111
+ } else {
112
+ throw e;
113
+ }
114
+ }
115
+ };
116
+ }
117
+ /**
118
+ * 再接続を試みる
119
+ */
120
+ attemptReconnect() {
121
+ const maxAttempts = this.options.maxReconnectAttempts ?? Infinity;
122
+ if (this.reconnectAttempts >= maxAttempts) {
123
+ return;
124
+ }
125
+ this.reconnectAttempts++;
126
+ this.reconnectCallbacks.forEach((cb) => cb(this.reconnectAttempts));
127
+ const interval = this.options.reconnectInterval ?? 3e3;
128
+ this.reconnectTimer = setTimeout(() => {
129
+ this.isFirstMessage = true;
130
+ this._uid = null;
131
+ this.createConnection();
132
+ }, interval);
133
+ }
134
+ /**
135
+ * WebSocket接続完了イベント登録
136
+ * @param callback 接続時に呼ばれるコールバック
137
+ */
138
+ onConnect(callback) {
139
+ this.connectCallbacks.push(callback);
140
+ if (this._uid) {
141
+ callback(this._uid);
142
+ }
143
+ }
144
+ /**
145
+ * WebSocket再接続イベント登録
146
+ * @param callback 再接続試行時に呼ばれるコールバック
147
+ */
148
+ onReconnect(callback) {
149
+ this.reconnectCallbacks.push(callback);
150
+ }
151
+ /**
152
+ * UID
153
+ */
154
+ get uid() {
155
+ return this._uid;
156
+ }
157
+ /**
158
+ * クライアントインスタンスを取得
159
+ */
160
+ get client() {
161
+ return this._client;
162
+ }
163
+ /**
164
+ * 接続状態を取得
165
+ */
166
+ get isConnected() {
167
+ return this._client.readyState === WS_OPEN;
168
+ }
169
+ /**
170
+ * WebSocketエラーイベント登録
171
+ * @param callback エラー時に呼ばれるコールバック
172
+ */
173
+ onError(callback) {
174
+ this.errorCallbacks.push(callback);
175
+ }
176
+ /**
177
+ * WebSocketクローズイベント登録
178
+ * @param callback クローズ時に呼ばれるコールバック
179
+ */
180
+ onClose(callback) {
181
+ this.onCloseCallback = callback;
182
+ }
183
+ on(channel, addressOrCallback, callback) {
184
+ const address = typeof addressOrCallback === "string" ? addressOrCallback : void 0;
185
+ const actualCallback = typeof addressOrCallback === "function" ? addressOrCallback : callback;
186
+ const channelPath = symbolChannelPaths[channel];
187
+ const subscribePath = typeof channelPath.subscribe === "function" ? channelPath.subscribe(address) : channelPath.subscribe;
188
+ if (!subscribePath) {
189
+ throw new Error(`Subscribe path could not be determined for channel: ${channel}`);
190
+ }
191
+ if (!this.eventCallbacks[subscribePath]) {
192
+ this.eventCallbacks[subscribePath] = [];
193
+ }
194
+ this.eventCallbacks[subscribePath].push(actualCallback);
195
+ if (!this._uid) {
196
+ this.pendingSubscribes.push({ subscribePath, callback: actualCallback });
197
+ return;
198
+ }
199
+ if (this._client.readyState === WS_OPEN) {
200
+ this._client.send(JSON.stringify({ uid: this._uid, subscribe: subscribePath }));
201
+ this.activeSubscriptions.add(subscribePath);
202
+ }
203
+ }
204
+ off(channel, address) {
205
+ const channelPath = symbolChannelPaths[channel];
206
+ const subscribePath = typeof channelPath.subscribe === "function" ? channelPath.subscribe(address) : channelPath.subscribe;
207
+ if (!subscribePath) {
208
+ throw new Error(`Subscribe path could not be determined for channel: ${channel}`);
209
+ }
210
+ delete this.eventCallbacks[subscribePath];
211
+ this.activeSubscriptions.delete(subscribePath);
212
+ if (this._uid && this._client.readyState === WS_OPEN) {
213
+ this._client.send(JSON.stringify({ uid: this._uid, unsubscribe: subscribePath }));
214
+ }
215
+ }
216
+ /**
217
+ * WebSocket接続を切断
218
+ */
219
+ disconnect() {
220
+ this.isManualDisconnect = true;
221
+ if (this.reconnectTimer) {
222
+ clearTimeout(this.reconnectTimer);
223
+ this.reconnectTimer = null;
224
+ }
225
+ if (this.connectionTimeoutTimer) {
226
+ clearTimeout(this.connectionTimeoutTimer);
227
+ this.connectionTimeoutTimer = null;
228
+ }
229
+ this.eventCallbacks = {};
230
+ this.pendingSubscribes = [];
231
+ this.errorCallbacks = [];
232
+ this.connectCallbacks = [];
233
+ this.reconnectCallbacks = [];
234
+ this.activeSubscriptions.clear();
235
+ if (this._client.readyState === WS_OPEN || this._client.readyState === WS_CONNECTING) {
236
+ this._client.close();
237
+ }
238
+ this._uid = null;
239
+ this.isFirstMessage = true;
240
+ this.reconnectAttempts = 0;
241
+ }
242
+ };
243
+ export {
244
+ SymbolWebSocket,
245
+ symbolChannelPaths
246
+ };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@nemnesia/symbol-websocket",
3
+ "version": "0.1.0",
4
+ "description": "Symbol WebSocket Client Library",
5
+ "keywords": [
6
+ "nem",
7
+ "symbol",
8
+ "blockchain",
9
+ "websocket",
10
+ "nodejs"
11
+ ],
12
+ "author": "ccHarvestasya",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/nemnesia/symbol-tools.git",
17
+ "directory": "packages/symbol-websocket"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/nemnesia/symbol-tools/issues"
21
+ },
22
+ "homepage": "https://github.com/nemnesia/symbol-tools/tree/main/packages/symbol-websocket#readme",
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "main": "./dist/index.cjs",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ "import": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "require": {
36
+ "types": "./dist/index.d.cts",
37
+ "default": "./dist/index.cjs"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist/**/*",
42
+ "README.md",
43
+ "LICENSE",
44
+ "CHANGELOG.md"
45
+ ],
46
+ "dependencies": {
47
+ "@stomp/stompjs": "^7.2.1",
48
+ "isomorphic-ws": "^5.0.0",
49
+ "ws": "^8.18.3"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "volta": {
55
+ "node": "20.19.6"
56
+ },
57
+ "scripts": {
58
+ "build": "pnpm run clean && tsup",
59
+ "clean": "rm -rf dist",
60
+ "lint": "eslint \"{src,test,e2e}/**/*.ts\"",
61
+ "format": "prettier --write \"{src,test,e2e}/**/*.ts\"",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest",
64
+ "test:coverage": "vitest run --coverage",
65
+ "test:ui": "vitest --ui --coverage",
66
+ "publish:dryrun": "npm publish --dry-run",
67
+ "publish:release": "npm run build && npm publish"
68
+ }
69
+ }