@sayue_ltr/fleq 1.49.2

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 (52) hide show
  1. package/CHANGELOG.md +898 -0
  2. package/LICENSE +21 -0
  3. package/README.md +535 -0
  4. package/assets/icons/.gitkeep +0 -0
  5. package/assets/sounds/.gitkeep +0 -0
  6. package/assets/sounds/cancel.mp3 +0 -0
  7. package/assets/sounds/critical.mp3 +0 -0
  8. package/assets/sounds/info.mp3 +0 -0
  9. package/assets/sounds/normal.mp3 +0 -0
  10. package/assets/sounds/warning.mp3 +0 -0
  11. package/dist/config.js +638 -0
  12. package/dist/dmdata/connection-manager.js +2 -0
  13. package/dist/dmdata/endpoint-selector.js +185 -0
  14. package/dist/dmdata/multi-connection-manager.js +158 -0
  15. package/dist/dmdata/rest-client.js +281 -0
  16. package/dist/dmdata/telegram-parser.js +704 -0
  17. package/dist/dmdata/volcano-parser.js +647 -0
  18. package/dist/dmdata/ws-client.js +336 -0
  19. package/dist/engine/cli/cli-init.js +266 -0
  20. package/dist/engine/cli/cli-run.js +121 -0
  21. package/dist/engine/cli/cli.js +121 -0
  22. package/dist/engine/eew/eew-logger.js +355 -0
  23. package/dist/engine/eew/eew-tracker.js +229 -0
  24. package/dist/engine/messages/message-router.js +261 -0
  25. package/dist/engine/messages/tsunami-state.js +96 -0
  26. package/dist/engine/messages/volcano-state.js +131 -0
  27. package/dist/engine/messages/volcano-vfvo53-aggregator.js +173 -0
  28. package/dist/engine/monitor/monitor.js +118 -0
  29. package/dist/engine/monitor/repl-coordinator.js +63 -0
  30. package/dist/engine/monitor/shutdown.js +114 -0
  31. package/dist/engine/notification/node-notifier-loader.js +19 -0
  32. package/dist/engine/notification/notifier.js +338 -0
  33. package/dist/engine/notification/sound-player.js +230 -0
  34. package/dist/engine/notification/volcano-presentation.js +166 -0
  35. package/dist/engine/startup/config-resolver.js +139 -0
  36. package/dist/engine/startup/tsunami-initializer.js +91 -0
  37. package/dist/engine/startup/update-checker.js +229 -0
  38. package/dist/engine/startup/volcano-initializer.js +89 -0
  39. package/dist/index.js +24 -0
  40. package/dist/logger.js +95 -0
  41. package/dist/types.js +61 -0
  42. package/dist/ui/earthquake-formatter.js +871 -0
  43. package/dist/ui/eew-formatter.js +335 -0
  44. package/dist/ui/formatter.js +689 -0
  45. package/dist/ui/repl.js +2059 -0
  46. package/dist/ui/test-samples.js +880 -0
  47. package/dist/ui/theme.js +516 -0
  48. package/dist/ui/volcano-formatter.js +667 -0
  49. package/dist/ui/waiting-tips.js +227 -0
  50. package/dist/utils/intensity.js +13 -0
  51. package/dist/utils/secrets.js +14 -0
  52. package/package.json +69 -0
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VolcanoStateHolder = void 0;
4
+ const theme_1 = require("../../ui/theme");
5
+ /** レベルに対応するテーマロール */
6
+ function levelToRole(level) {
7
+ switch (level) {
8
+ case 5: return "frameCritical";
9
+ case 4: return "frameCritical";
10
+ case 3: return "frameWarning";
11
+ case 2: return "frameWarning";
12
+ case 1: return "frameNormal";
13
+ default: return "frameNormal";
14
+ }
15
+ }
16
+ /** レベルを表示文字列に変換 */
17
+ function levelToLabel(level) {
18
+ if (level == null)
19
+ return "";
20
+ return ` Lv${level}`;
21
+ }
22
+ /**
23
+ * 火山情報の状態を保持し、プロンプト表示と detail コマンドを提供する。
24
+ * 複数火山の同時追跡に対応 (volcanoCode をキーとする Map)。
25
+ */
26
+ class VolcanoStateHolder {
27
+ category = "volcano";
28
+ emptyMessage = "現在、継続中の火山警報はありません。";
29
+ entries = new Map();
30
+ /** VFVO50/VFSVii 受信時に状態を更新する */
31
+ update(info) {
32
+ // alert 系のみ状態追跡
33
+ if (info.kind !== "alert")
34
+ return;
35
+ // volcanoCode が欠落している場合はスキップ (Map キー衝突を防ぐ)
36
+ if (!info.volcanoCode)
37
+ return;
38
+ const alertInfo = info;
39
+ // 取消報 → エントリ削除
40
+ if (info.infoType === "取消") {
41
+ this.entries.delete(info.volcanoCode);
42
+ return;
43
+ }
44
+ // 解除 → エントリ削除
45
+ if (alertInfo.action === "release") {
46
+ this.entries.delete(info.volcanoCode);
47
+ return;
48
+ }
49
+ // レベル1で継続 → エントリ削除 (通常状態)
50
+ if (alertInfo.alertLevel === 1 && alertInfo.action === "continue") {
51
+ this.entries.delete(info.volcanoCode);
52
+ return;
53
+ }
54
+ this.entries.set(info.volcanoCode, {
55
+ volcanoCode: info.volcanoCode,
56
+ volcanoName: info.volcanoName,
57
+ alertLevel: alertInfo.alertLevel,
58
+ alertLevelCode: alertInfo.alertLevelCode,
59
+ action: alertInfo.action,
60
+ reportDateTime: info.reportDateTime,
61
+ lastInfo: alertInfo,
62
+ });
63
+ }
64
+ /**
65
+ * 同一火山で alertLevel・alertLevelCode・action が全て同じ場合 → 再通知と判定。
66
+ * 新規 or 変化あり → false
67
+ */
68
+ isRenotification(info) {
69
+ const existing = this.entries.get(info.volcanoCode);
70
+ if (!existing)
71
+ return false;
72
+ return (existing.alertLevel === info.alertLevel &&
73
+ existing.alertLevelCode === info.alertLevelCode &&
74
+ existing.action === info.action);
75
+ }
76
+ /** 状態をクリアする */
77
+ clear() {
78
+ this.entries.clear();
79
+ }
80
+ /** エントリ数を返す (テスト用) */
81
+ size() {
82
+ return this.entries.size;
83
+ }
84
+ /** 指定火山のエントリを返す (テスト用) */
85
+ getEntry(volcanoCode) {
86
+ return this.entries.get(volcanoCode);
87
+ }
88
+ // ── PromptStatusProvider ──
89
+ getPromptStatus() {
90
+ if (this.entries.size === 0)
91
+ return null;
92
+ // 最も高い alertLevel のエントリを選択
93
+ let highest = null;
94
+ for (const entry of this.entries.values()) {
95
+ if (!highest || (entry.alertLevel ?? 0) > (highest.alertLevel ?? 0)) {
96
+ highest = entry;
97
+ }
98
+ }
99
+ if (!highest)
100
+ return null;
101
+ const role = levelToRole(highest.alertLevel);
102
+ const colorFn = (0, theme_1.getRoleChalk)(role);
103
+ const label = `${highest.volcanoName}${levelToLabel(highest.alertLevel)}`;
104
+ return {
105
+ text: colorFn(label),
106
+ priority: 20,
107
+ };
108
+ }
109
+ // ── DetailProvider ──
110
+ hasDetail() {
111
+ return this.entries.size > 0;
112
+ }
113
+ showDetail() {
114
+ if (this.entries.size === 0)
115
+ return;
116
+ console.log("");
117
+ console.log(" 継続中の火山警報:");
118
+ console.log("");
119
+ const sorted = [...this.entries.values()].sort((a, b) => (b.alertLevel ?? 0) - (a.alertLevel ?? 0));
120
+ for (const entry of sorted) {
121
+ const role = levelToRole(entry.alertLevel);
122
+ const colorFn = (0, theme_1.getRoleChalk)(role);
123
+ const levelStr = entry.alertLevel != null
124
+ ? `Lv${entry.alertLevel}`
125
+ : entry.alertLevelCode ?? "—";
126
+ console.log(` ${colorFn(entry.volcanoName)} ${colorFn(levelStr)} ${entry.lastInfo.warningKind}`);
127
+ }
128
+ console.log("");
129
+ }
130
+ }
131
+ exports.VolcanoStateHolder = VolcanoStateHolder;
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ /**
3
+ * VFVO53(降灰予報・定時)アグリゲータ
4
+ *
5
+ * 定時で一斉に届く複数火山の VFVO53 をバッファリングし、
6
+ * まとめて1フレームとして表示・通知するための集約モジュール。
7
+ *
8
+ * - バッチキー: reportDateTime + isTest で同一発表サイクルをグルーピング
9
+ * - 同一火山は volcanoCode で上書き(訂正/重複対応)
10
+ * - 取消電文は即時表示 + バッファから除去
11
+ * - VFVO54 等の割り込みはバッファを通知なし flush してから通常処理
12
+ * - 単発なら既存の単発表示にフォールバック
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.VolcanoVfvo53Aggregator = void 0;
49
+ const log = __importStar(require("../../logger"));
50
+ // ── デフォルト定数 ──
51
+ const DEFAULT_QUIET_MS = 8_000;
52
+ const DEFAULT_MAX_WAIT_MS = 90_000;
53
+ const DEFAULT_MAX_ITEMS = 20;
54
+ // ── 本体 ──
55
+ class VolcanoVfvo53Aggregator {
56
+ emitSingle;
57
+ emitBatch;
58
+ timer = null;
59
+ batchKey = null;
60
+ startedAt = 0;
61
+ items = new Map();
62
+ disposed = false;
63
+ quietMs;
64
+ maxWaitMs;
65
+ maxItems;
66
+ constructor(emitSingle, emitBatch, opts) {
67
+ this.emitSingle = emitSingle;
68
+ this.emitBatch = emitBatch;
69
+ this.quietMs = opts?.quietMs ?? DEFAULT_QUIET_MS;
70
+ this.maxWaitMs = opts?.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
71
+ this.maxItems = opts?.maxItems ?? DEFAULT_MAX_ITEMS;
72
+ }
73
+ /** 火山電文を処理する。VFVO53 定時ならバッファリング、それ以外は即時委譲 */
74
+ handle(info) {
75
+ if (this.disposed) {
76
+ this.emitSingle(info);
77
+ return;
78
+ }
79
+ // VFVO53 ashfall の判定
80
+ if (info.kind === "ashfall" && info.type === "VFVO53") {
81
+ // 取消電文: 即時表示 + バッファから該当火山を除去
82
+ if (info.infoType === "取消") {
83
+ this.removeCancelled(info);
84
+ this.emitSingle(info);
85
+ return;
86
+ }
87
+ // 定時 → バッファリング対象
88
+ this.buffer(info);
89
+ return;
90
+ }
91
+ // その他の火山電文: pending があれば通知なし flush してから即時委譲
92
+ if (this.items.size > 0) {
93
+ this.flush("interrupt", { notify: false });
94
+ }
95
+ this.emitSingle(info);
96
+ }
97
+ /** 保留中のバッファを全 flush し、タイマーを破棄する */
98
+ flushAndDispose() {
99
+ if (this.disposed)
100
+ return;
101
+ this.disposed = true;
102
+ if (this.items.size > 0) {
103
+ this.flush("dispose", { notify: true });
104
+ }
105
+ this.clearTimer();
106
+ }
107
+ // ── private ──
108
+ buffer(info) {
109
+ const key = `${info.reportDateTime}:${info.isTest}`;
110
+ // バッチキー不一致 → 先行バッチを flush してから新バッチ開始
111
+ if (this.batchKey !== null && this.batchKey !== key) {
112
+ this.flush("newBatchKey", { notify: true });
113
+ }
114
+ const now = Date.now();
115
+ if (this.batchKey === null) {
116
+ this.startedAt = now;
117
+ }
118
+ this.batchKey = key;
119
+ this.items.set(info.volcanoCode, info);
120
+ // maxItems 到達で即 flush
121
+ if (this.items.size >= this.maxItems) {
122
+ this.flush("maxItems", { notify: true });
123
+ return;
124
+ }
125
+ this.armTimer();
126
+ }
127
+ removeCancelled(info) {
128
+ if (this.items.has(info.volcanoCode)) {
129
+ this.items.delete(info.volcanoCode);
130
+ log.debug(`VFVO53 aggregator: removed cancelled ${info.volcanoCode} from buffer`);
131
+ if (this.items.size === 0) {
132
+ this.clearTimer();
133
+ this.batchKey = null;
134
+ this.startedAt = 0;
135
+ }
136
+ }
137
+ }
138
+ armTimer() {
139
+ this.clearTimer();
140
+ const elapsed = Date.now() - this.startedAt;
141
+ const remainMax = this.maxWaitMs - elapsed;
142
+ const delay = Math.max(10, Math.min(this.quietMs, remainMax));
143
+ this.timer = setTimeout(() => {
144
+ const reason = delay < this.quietMs ? "maxWait" : "quiet";
145
+ this.flush(reason, { notify: true });
146
+ }, delay);
147
+ }
148
+ flush(reason, opts) {
149
+ this.clearTimer();
150
+ const items = [...this.items.values()].sort((a, b) => a.volcanoName.localeCompare(b.volcanoName, "ja"));
151
+ log.debug(`VFVO53 aggregator: flush reason=${reason}, count=${items.length}`);
152
+ if (items.length === 1) {
153
+ this.emitSingle(items[0], opts);
154
+ }
155
+ else if (items.length > 1) {
156
+ this.emitBatch({
157
+ reportDateTime: items[0].reportDateTime,
158
+ isTest: items[0].isTest,
159
+ items,
160
+ }, opts);
161
+ }
162
+ this.items.clear();
163
+ this.batchKey = null;
164
+ this.startedAt = 0;
165
+ }
166
+ clearTimer() {
167
+ if (this.timer !== null) {
168
+ clearTimeout(this.timer);
169
+ this.timer = null;
170
+ }
171
+ }
172
+ }
173
+ exports.VolcanoVfvo53Aggregator = VolcanoVfvo53Aggregator;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.startMonitor = startMonitor;
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const multi_connection_manager_1 = require("../../dmdata/multi-connection-manager");
42
+ const message_router_1 = require("../messages/message-router");
43
+ const tsunami_initializer_1 = require("../startup/tsunami-initializer");
44
+ const volcano_initializer_1 = require("../startup/volcano-initializer");
45
+ const cli_run_1 = require("../cli/cli-run");
46
+ const formatter_1 = require("../../ui/formatter");
47
+ const repl_coordinator_1 = require("./repl-coordinator");
48
+ const shutdown_1 = require("./shutdown");
49
+ const log = __importStar(require("../../logger"));
50
+ async function startMonitor(config) {
51
+ const { handler: routeMessage, eewLogger, notifier, tsunamiState, volcanoState, flushAndDisposeVolcanoBuffer } = (0, message_router_1.createMessageHandler)();
52
+ // EEW ログ設定を反映
53
+ eewLogger.setEnabled(config.eewLog);
54
+ eewLogger.setFields(config.eewLogFields);
55
+ let disconnectedAt = null;
56
+ let isFirstConnection = true;
57
+ let replHandler = null;
58
+ const manager = new multi_connection_manager_1.MultiConnectionManager(config, {
59
+ onData: (msg) => {
60
+ (0, repl_coordinator_1.withReplDisplay)(replHandler, () => routeMessage(msg));
61
+ },
62
+ onConnected: () => {
63
+ // 再接続時: 切断期間の通知
64
+ if (disconnectedAt != null) {
65
+ const gapStart = (0, formatter_1.formatTimestamp)(new Date(disconnectedAt).toISOString());
66
+ const gapEnd = (0, formatter_1.formatTimestamp)(new Date().toISOString());
67
+ log.warn(`${gapStart} 〜 ${gapEnd} の間、電文を受信できていない可能性があります`);
68
+ disconnectedAt = null;
69
+ }
70
+ log.info(chalk_1.default.green("リアルタイム受信中..."));
71
+ if (isFirstConnection) {
72
+ log.info(chalk_1.default.gray("help でコマンド一覧を表示"));
73
+ isFirstConnection = false;
74
+ }
75
+ (0, repl_coordinator_1.updateReplConnectionState)(replHandler, true);
76
+ },
77
+ onDisconnected: (reason) => {
78
+ disconnectedAt = Date.now();
79
+ log.warn(`切断されました: ${reason}`);
80
+ (0, repl_coordinator_1.updateReplConnectionState)(replHandler, false);
81
+ },
82
+ });
83
+ // グレースフルシャットダウン
84
+ const shutdown = (0, shutdown_1.createShutdownHandler)({
85
+ apiKey: config.apiKey,
86
+ manager,
87
+ eewLogger,
88
+ getReplHandler: () => replHandler,
89
+ resetTerminalTitle: cli_run_1.resetTerminalTitle,
90
+ flushAndDisposeVolcanoBuffer,
91
+ });
92
+ // REPL ハンドラ (遅延ロード)
93
+ const { ReplHandler } = await Promise.resolve().then(() => __importStar(require("../../ui/repl")));
94
+ replHandler = new ReplHandler(config, manager, notifier, eewLogger, shutdown, [tsunamiState, volcanoState], [tsunamiState, volcanoState]);
95
+ (0, shutdown_1.registerShutdownSignals)(shutdown);
96
+ // REPL を先に起動 (接続中もコマンド入力可能にする)
97
+ replHandler.start();
98
+ // 起動時: 最新の津波・火山警報状態を復元 (WebSocket 接続前に実行)
99
+ await (0, tsunami_initializer_1.restoreTsunamiState)(config.apiKey, tsunamiState);
100
+ await (0, volcano_initializer_1.restoreVolcanoState)(config.apiKey, volcanoState);
101
+ // バックグラウンドで接続開始
102
+ try {
103
+ await manager.connect();
104
+ // 副回線の自動起動
105
+ if (config.backup) {
106
+ try {
107
+ await manager.startBackup();
108
+ }
109
+ catch (err) {
110
+ log.warn(`副回線の起動に失敗しました: ${err instanceof Error ? err.message : err}`);
111
+ }
112
+ }
113
+ }
114
+ catch (err) {
115
+ log.error(`接続に失敗しました: ${err instanceof Error ? err.message : err}`);
116
+ log.info("retry コマンドで再接続を試みることができます。");
117
+ }
118
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.withReplDisplay = withReplDisplay;
37
+ exports.updateReplConnectionState = updateReplConnectionState;
38
+ const log = __importStar(require("../../logger"));
39
+ /**
40
+ * REPL の表示状態を一時的に制御しつつアクションを実行する。
41
+ * 電文表示時にプロンプトが割り込まないよう beforeDisplayMessage / afterDisplayMessage で囲む。
42
+ */
43
+ function withReplDisplay(repl, action) {
44
+ repl?.beforeDisplayMessage();
45
+ try {
46
+ action();
47
+ }
48
+ catch (err) {
49
+ log.error(`電文処理エラー: ${err instanceof Error ? err.message : err}`);
50
+ }
51
+ finally {
52
+ repl?.afterDisplayMessage();
53
+ }
54
+ }
55
+ /**
56
+ * REPL の接続状態を更新してプロンプトを再描画する。
57
+ */
58
+ function updateReplConnectionState(repl, connected) {
59
+ if (!repl)
60
+ return;
61
+ repl.setConnected(connected);
62
+ repl.refreshPrompt();
63
+ }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createShutdownHandler = createShutdownHandler;
37
+ exports.registerShutdownSignals = registerShutdownSignals;
38
+ const rest_client_1 = require("../../dmdata/rest-client");
39
+ const log = __importStar(require("../../logger"));
40
+ const SOCKET_CLOSE_TIMEOUT_MS = 3000;
41
+ /** 構造的型ガード: getAllSocketIds メソッドを持つか */
42
+ function hasGetAllSocketIds(m) {
43
+ return "getAllSocketIds" in m && typeof m["getAllSocketIds"] === "function";
44
+ }
45
+ /** 単一ソケットを API 経由でクローズする (タイムアウト付き) */
46
+ async function closeSingleSocket(apiKey, socketId) {
47
+ try {
48
+ await Promise.race([
49
+ (0, rest_client_1.closeSocket)(apiKey, socketId),
50
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), SOCKET_CLOSE_TIMEOUT_MS)),
51
+ ]);
52
+ }
53
+ catch {
54
+ log.debug(`シャットダウン時のソケットクローズに失敗: socketId=${socketId} (次回起動時にクリーンアップされます)`);
55
+ }
56
+ }
57
+ /**
58
+ * API 経由でソケットをクローズする。
59
+ * MultiConnectionManager の場合は全ソケットを並列クローズする。
60
+ * タイムアウトやネットワークエラーは無視して終了を続行する。
61
+ */
62
+ async function closeSocketViaApi(apiKey, manager) {
63
+ if (hasGetAllSocketIds(manager)) {
64
+ const socketIds = manager.getAllSocketIds();
65
+ if (socketIds.length === 0)
66
+ return;
67
+ await Promise.all(socketIds.map((id) => closeSingleSocket(apiKey, id)));
68
+ }
69
+ else {
70
+ const socketId = manager.getStatus().socketId;
71
+ if (socketId == null)
72
+ return;
73
+ await closeSingleSocket(apiKey, socketId);
74
+ }
75
+ }
76
+ /**
77
+ * グレースフルシャットダウンハンドラを生成する。
78
+ * 返された関数は複数回呼ばれても冪等 (二重シャットダウン防止)。
79
+ */
80
+ function createShutdownHandler(ctx) {
81
+ let shuttingDown = false;
82
+ return async () => {
83
+ if (shuttingDown)
84
+ return;
85
+ shuttingDown = true;
86
+ log.info("シャットダウン中...");
87
+ ctx.flushAndDisposeVolcanoBuffer?.();
88
+ ctx.eewLogger.closeAll();
89
+ try {
90
+ await ctx.eewLogger.flush();
91
+ }
92
+ catch {
93
+ // flush 失敗は無視
94
+ }
95
+ const repl = ctx.getReplHandler();
96
+ if (repl)
97
+ repl.stop();
98
+ const socketClosePromise = closeSocketViaApi(ctx.apiKey, ctx.manager);
99
+ ctx.manager.close();
100
+ await socketClosePromise;
101
+ ctx.resetTerminalTitle();
102
+ if (process.stdout.isTTY)
103
+ process.stdout.write("\n");
104
+ process.exit(0);
105
+ };
106
+ }
107
+ /** シャットダウンシグナルを登録する */
108
+ function registerShutdownSignals(shutdown) {
109
+ process.on("SIGINT", shutdown);
110
+ process.on("SIGTERM", shutdown);
111
+ if (process.platform !== "win32") {
112
+ process.on("SIGHUP", shutdown);
113
+ }
114
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setNodeNotifierOverride = setNodeNotifierOverride;
4
+ exports.loadNodeNotifier = loadNodeNotifier;
5
+ let nodeNotifierOverride;
6
+ function setNodeNotifierOverride(notifier) {
7
+ nodeNotifierOverride = notifier;
8
+ }
9
+ function loadNodeNotifier() {
10
+ if (nodeNotifierOverride !== undefined) {
11
+ return nodeNotifierOverride;
12
+ }
13
+ try {
14
+ return require("node-notifier");
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }