@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.
- package/CHANGELOG.md +898 -0
- package/LICENSE +21 -0
- package/README.md +535 -0
- package/assets/icons/.gitkeep +0 -0
- package/assets/sounds/.gitkeep +0 -0
- package/assets/sounds/cancel.mp3 +0 -0
- package/assets/sounds/critical.mp3 +0 -0
- package/assets/sounds/info.mp3 +0 -0
- package/assets/sounds/normal.mp3 +0 -0
- package/assets/sounds/warning.mp3 +0 -0
- package/dist/config.js +638 -0
- package/dist/dmdata/connection-manager.js +2 -0
- package/dist/dmdata/endpoint-selector.js +185 -0
- package/dist/dmdata/multi-connection-manager.js +158 -0
- package/dist/dmdata/rest-client.js +281 -0
- package/dist/dmdata/telegram-parser.js +704 -0
- package/dist/dmdata/volcano-parser.js +647 -0
- package/dist/dmdata/ws-client.js +336 -0
- package/dist/engine/cli/cli-init.js +266 -0
- package/dist/engine/cli/cli-run.js +121 -0
- package/dist/engine/cli/cli.js +121 -0
- package/dist/engine/eew/eew-logger.js +355 -0
- package/dist/engine/eew/eew-tracker.js +229 -0
- package/dist/engine/messages/message-router.js +261 -0
- package/dist/engine/messages/tsunami-state.js +96 -0
- package/dist/engine/messages/volcano-state.js +131 -0
- package/dist/engine/messages/volcano-vfvo53-aggregator.js +173 -0
- package/dist/engine/monitor/monitor.js +118 -0
- package/dist/engine/monitor/repl-coordinator.js +63 -0
- package/dist/engine/monitor/shutdown.js +114 -0
- package/dist/engine/notification/node-notifier-loader.js +19 -0
- package/dist/engine/notification/notifier.js +338 -0
- package/dist/engine/notification/sound-player.js +230 -0
- package/dist/engine/notification/volcano-presentation.js +166 -0
- package/dist/engine/startup/config-resolver.js +139 -0
- package/dist/engine/startup/tsunami-initializer.js +91 -0
- package/dist/engine/startup/update-checker.js +229 -0
- package/dist/engine/startup/volcano-initializer.js +89 -0
- package/dist/index.js +24 -0
- package/dist/logger.js +95 -0
- package/dist/types.js +61 -0
- package/dist/ui/earthquake-formatter.js +871 -0
- package/dist/ui/eew-formatter.js +335 -0
- package/dist/ui/formatter.js +689 -0
- package/dist/ui/repl.js +2059 -0
- package/dist/ui/test-samples.js +880 -0
- package/dist/ui/theme.js +516 -0
- package/dist/ui/volcano-formatter.js +667 -0
- package/dist/ui/waiting-tips.js +227 -0
- package/dist/utils/intensity.js +13 -0
- package/dist/utils/secrets.js +14 -0
- package/package.json +69 -0
|
@@ -0,0 +1,185 @@
|
|
|
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.EndpointSelector = void 0;
|
|
37
|
+
const log = __importStar(require("../logger"));
|
|
38
|
+
/** リージョン別エンドポイント (冗長化用) */
|
|
39
|
+
const REGION_HOSTS = {
|
|
40
|
+
tokyo: "ws-tokyo.api.dmdata.jp",
|
|
41
|
+
osaka: "ws-osaka.api.dmdata.jp",
|
|
42
|
+
};
|
|
43
|
+
/** 個別サーバー → リージョンのマッピング */
|
|
44
|
+
const SERVER_TO_REGION = {
|
|
45
|
+
"ws001.api.dmdata.jp": "tokyo",
|
|
46
|
+
"ws002.api.dmdata.jp": "tokyo",
|
|
47
|
+
"ws003.api.dmdata.jp": "osaka",
|
|
48
|
+
"ws004.api.dmdata.jp": "osaka",
|
|
49
|
+
"ws-tokyo.api.dmdata.jp": "tokyo",
|
|
50
|
+
"ws-osaka.api.dmdata.jp": "osaka",
|
|
51
|
+
};
|
|
52
|
+
/** クールダウン初期値 (ミリ秒) */
|
|
53
|
+
const INITIAL_COOLDOWN_MS = 120_000;
|
|
54
|
+
/** クールダウン上限 (ミリ秒) */
|
|
55
|
+
const MAX_COOLDOWN_MS = 900_000;
|
|
56
|
+
/** 連続失敗判定の時間窓 (ミリ秒): この時間内に再度失敗するとクールダウンを延長 */
|
|
57
|
+
const REPEATED_FAILURE_WINDOW_MS = 600_000;
|
|
58
|
+
/**
|
|
59
|
+
* WebSocket エンドポイント選択器
|
|
60
|
+
*
|
|
61
|
+
* 切断時に失敗ホストを記録し、再接続時に別リージョンを優先する。
|
|
62
|
+
* Socket Start レスポンスの URL ホスト名を必要に応じて差し替える。
|
|
63
|
+
*/
|
|
64
|
+
class EndpointSelector {
|
|
65
|
+
/** ホストごとの失敗記録 */
|
|
66
|
+
failures = new Map();
|
|
67
|
+
/** 直前に接続していたホスト名 */
|
|
68
|
+
lastConnectedHost = null;
|
|
69
|
+
/**
|
|
70
|
+
* 接続成功時に呼ぶ。接続先ホストを記録する。
|
|
71
|
+
*/
|
|
72
|
+
recordConnected(wsUrl) {
|
|
73
|
+
const host = this.extractHost(wsUrl);
|
|
74
|
+
if (host == null)
|
|
75
|
+
return;
|
|
76
|
+
this.lastConnectedHost = host;
|
|
77
|
+
log.debug(`EndpointSelector: 接続先を記録: ${host}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 切断時に呼ぶ。失敗ホストを記録しクールダウンを設定する。
|
|
81
|
+
*/
|
|
82
|
+
recordDisconnected() {
|
|
83
|
+
const host = this.lastConnectedHost;
|
|
84
|
+
if (host == null)
|
|
85
|
+
return;
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
const existing = this.failures.get(host);
|
|
88
|
+
let cooldownMs;
|
|
89
|
+
if (existing != null &&
|
|
90
|
+
now - existing.failedAt < REPEATED_FAILURE_WINDOW_MS) {
|
|
91
|
+
// 時間窓内の再失敗 → クールダウンを延長 (上限あり)
|
|
92
|
+
cooldownMs = Math.min(existing.cooldownMs * 2.5, MAX_COOLDOWN_MS);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
cooldownMs = INITIAL_COOLDOWN_MS;
|
|
96
|
+
}
|
|
97
|
+
this.failures.set(host, { failedAt: now, cooldownMs });
|
|
98
|
+
log.info(`EndpointSelector: ${host} を ${(cooldownMs / 1000).toFixed(0)}秒間クールダウン`);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Socket Start レスポンスの URL を受け取り、必要に応じてホスト名を差し替える。
|
|
102
|
+
*
|
|
103
|
+
* - 返却ホストがクールダウン中 → 反対リージョンに差し替え
|
|
104
|
+
* - 返却ホストが直前の失敗ホストと同じ → 反対リージョンに差し替え
|
|
105
|
+
* - それ以外 → そのまま返す
|
|
106
|
+
*/
|
|
107
|
+
resolveUrl(originalUrl) {
|
|
108
|
+
this.pruneExpiredFailures();
|
|
109
|
+
const host = this.extractHost(originalUrl);
|
|
110
|
+
if (host == null)
|
|
111
|
+
return originalUrl;
|
|
112
|
+
const shouldAvoid = this.isInCooldown(host) || host === this.lastConnectedHost;
|
|
113
|
+
if (!shouldAvoid) {
|
|
114
|
+
log.debug(`EndpointSelector: ${host} をそのまま使用`);
|
|
115
|
+
return originalUrl;
|
|
116
|
+
}
|
|
117
|
+
// 反対リージョンを探す
|
|
118
|
+
const alternativeHost = this.findAlternativeHost(host);
|
|
119
|
+
if (alternativeHost == null) {
|
|
120
|
+
log.debug(`EndpointSelector: 代替ホストが見つからないため ${host} をそのまま使用`);
|
|
121
|
+
return originalUrl;
|
|
122
|
+
}
|
|
123
|
+
log.info(`EndpointSelector: ${host} を回避 → ${alternativeHost} に差し替え`);
|
|
124
|
+
return this.replaceHost(originalUrl, alternativeHost);
|
|
125
|
+
}
|
|
126
|
+
/** 指定ホストがクールダウン中かどうか */
|
|
127
|
+
isInCooldown(host) {
|
|
128
|
+
const record = this.failures.get(host);
|
|
129
|
+
if (record == null)
|
|
130
|
+
return false;
|
|
131
|
+
return Date.now() - record.failedAt < record.cooldownMs;
|
|
132
|
+
}
|
|
133
|
+
/** 期限切れの失敗記録を削除 */
|
|
134
|
+
pruneExpiredFailures() {
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
for (const [host, record] of this.failures) {
|
|
137
|
+
if (now - record.failedAt >= record.cooldownMs) {
|
|
138
|
+
this.failures.delete(host);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** 指定ホストの反対リージョンのエンドポイントを返す */
|
|
143
|
+
findAlternativeHost(currentHost) {
|
|
144
|
+
const currentRegion = this.detectRegion(currentHost);
|
|
145
|
+
// 反対リージョンを試す
|
|
146
|
+
const oppositeRegion = currentRegion === "tokyo" ? "osaka" : "tokyo";
|
|
147
|
+
const candidate = REGION_HOSTS[oppositeRegion];
|
|
148
|
+
// 反対リージョンもクールダウン中なら諦める
|
|
149
|
+
if (this.isInCooldown(candidate)) {
|
|
150
|
+
log.debug(`EndpointSelector: 反対リージョン ${candidate} もクールダウン中`);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return candidate;
|
|
154
|
+
}
|
|
155
|
+
/** ホスト名からリージョンを推定する */
|
|
156
|
+
detectRegion(host) {
|
|
157
|
+
const mapped = SERVER_TO_REGION[host];
|
|
158
|
+
if (mapped != null)
|
|
159
|
+
return mapped;
|
|
160
|
+
// ws.api.dmdata.jp や未知のホストはデフォルトで tokyo とみなす
|
|
161
|
+
return "tokyo";
|
|
162
|
+
}
|
|
163
|
+
/** URL からホスト名を抽出する */
|
|
164
|
+
extractHost(wsUrl) {
|
|
165
|
+
try {
|
|
166
|
+
const u = new URL(wsUrl);
|
|
167
|
+
return u.hostname;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** URL のホスト名を差し替える */
|
|
174
|
+
replaceHost(originalUrl, newHost) {
|
|
175
|
+
try {
|
|
176
|
+
const u = new URL(originalUrl);
|
|
177
|
+
u.hostname = newHost;
|
|
178
|
+
return u.toString();
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return originalUrl;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
exports.EndpointSelector = EndpointSelector;
|
|
@@ -0,0 +1,158 @@
|
|
|
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.MultiConnectionManager = void 0;
|
|
37
|
+
const ws_client_1 = require("./ws-client");
|
|
38
|
+
const log = __importStar(require("../logger"));
|
|
39
|
+
/** 重複排除の最大キープ数 */
|
|
40
|
+
const SEEN_IDS_MAX = 500;
|
|
41
|
+
/** EEW 関連の分類区分 */
|
|
42
|
+
const EEW_CLASSIFICATIONS = ["eew.forecast", "eew.warning"];
|
|
43
|
+
/**
|
|
44
|
+
* 複線接続管理。primary (通常回線) に加え、backup (EEW 副回線) を動的に起動/停止できる。
|
|
45
|
+
* backup からの受信は msg.id で重複排除した上で、同じ onData イベントに委譲する。
|
|
46
|
+
*/
|
|
47
|
+
class MultiConnectionManager {
|
|
48
|
+
primary;
|
|
49
|
+
backup = null;
|
|
50
|
+
config;
|
|
51
|
+
events;
|
|
52
|
+
seenIds = new Set();
|
|
53
|
+
seenOrder = [];
|
|
54
|
+
constructor(config, events) {
|
|
55
|
+
this.config = config;
|
|
56
|
+
this.events = events;
|
|
57
|
+
this.primary = new ws_client_1.WebSocketManager(config, {
|
|
58
|
+
onData: (msg) => this.handleData(msg),
|
|
59
|
+
onConnected: events.onConnected,
|
|
60
|
+
onDisconnected: events.onDisconnected,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** primary の接続を開始する */
|
|
64
|
+
async connect() {
|
|
65
|
+
await this.primary.connect();
|
|
66
|
+
}
|
|
67
|
+
/** primary の接続状態を返す */
|
|
68
|
+
getStatus() {
|
|
69
|
+
return this.primary.getStatus();
|
|
70
|
+
}
|
|
71
|
+
/** primary と backup の両方を停止する */
|
|
72
|
+
close() {
|
|
73
|
+
this.primary.close();
|
|
74
|
+
if (this.backup) {
|
|
75
|
+
this.backup.close();
|
|
76
|
+
this.backup = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** 全ソケットの ID を返す (シャットダウン時の API クローズ用) */
|
|
80
|
+
getAllSocketIds() {
|
|
81
|
+
const ids = [];
|
|
82
|
+
const primaryId = this.primary.getStatus().socketId;
|
|
83
|
+
if (primaryId != null)
|
|
84
|
+
ids.push(primaryId);
|
|
85
|
+
if (this.backup) {
|
|
86
|
+
const backupId = this.backup.getStatus().socketId;
|
|
87
|
+
if (backupId != null)
|
|
88
|
+
ids.push(backupId);
|
|
89
|
+
}
|
|
90
|
+
return ids;
|
|
91
|
+
}
|
|
92
|
+
/** backup の接続状態を返す (未起動時は null) */
|
|
93
|
+
getBackupStatus() {
|
|
94
|
+
return this.backup?.getStatus() ?? null;
|
|
95
|
+
}
|
|
96
|
+
/** backup が起動中か */
|
|
97
|
+
isBackupRunning() {
|
|
98
|
+
return this.backup != null;
|
|
99
|
+
}
|
|
100
|
+
/** EEW 副回線を起動する。戻り値で起動結果を返す */
|
|
101
|
+
async startBackup() {
|
|
102
|
+
if (this.backup) {
|
|
103
|
+
log.warn("副回線は既に起動中です");
|
|
104
|
+
return "already_running";
|
|
105
|
+
}
|
|
106
|
+
// EEW 区分と契約済み区分の積集合
|
|
107
|
+
const backupClassifications = this.config.classifications.filter((c) => EEW_CLASSIFICATIONS.includes(c));
|
|
108
|
+
if (backupClassifications.length === 0) {
|
|
109
|
+
log.warn("EEW 区分 (eew.forecast, eew.warning) が契約に含まれていないため、副回線を起動できません");
|
|
110
|
+
return "no_eew_contract";
|
|
111
|
+
}
|
|
112
|
+
const backupConfig = {
|
|
113
|
+
...this.config,
|
|
114
|
+
classifications: backupClassifications,
|
|
115
|
+
appName: `${this.config.appName}-backup`,
|
|
116
|
+
keepExistingConnections: true,
|
|
117
|
+
};
|
|
118
|
+
this.backup = new ws_client_1.WebSocketManager(backupConfig, {
|
|
119
|
+
onData: (msg) => this.handleData(msg),
|
|
120
|
+
onConnected: () => {
|
|
121
|
+
log.info("副回線: 接続成功");
|
|
122
|
+
},
|
|
123
|
+
onDisconnected: (reason) => {
|
|
124
|
+
log.warn(`副回線: 切断 — ${reason}`);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
log.info("副回線を起動中...");
|
|
128
|
+
await this.backup.connect();
|
|
129
|
+
return "started";
|
|
130
|
+
}
|
|
131
|
+
/** EEW 副回線を停止する */
|
|
132
|
+
stopBackup() {
|
|
133
|
+
if (!this.backup) {
|
|
134
|
+
log.warn("副回線は起動していません");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.backup.close();
|
|
138
|
+
this.backup = null;
|
|
139
|
+
log.info("副回線を停止しました");
|
|
140
|
+
}
|
|
141
|
+
/** 重複排除付きデータハンドラ */
|
|
142
|
+
handleData(msg) {
|
|
143
|
+
if (this.seenIds.has(msg.id)) {
|
|
144
|
+
log.debug(`重複排除: id=${msg.id.slice(0, 16)}...`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// FIFO window: 古い ID を先頭から削除
|
|
148
|
+
this.seenIds.add(msg.id);
|
|
149
|
+
this.seenOrder.push(msg.id);
|
|
150
|
+
while (this.seenOrder.length > SEEN_IDS_MAX) {
|
|
151
|
+
const oldest = this.seenOrder.shift();
|
|
152
|
+
if (oldest)
|
|
153
|
+
this.seenIds.delete(oldest);
|
|
154
|
+
}
|
|
155
|
+
this.events.onData(msg);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.MultiConnectionManager = MultiConnectionManager;
|
|
@@ -0,0 +1,281 @@
|
|
|
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.listContracts = listContracts;
|
|
40
|
+
exports.listEarthquakes = listEarthquakes;
|
|
41
|
+
exports.listTelegrams = listTelegrams;
|
|
42
|
+
exports.listSockets = listSockets;
|
|
43
|
+
exports.closeSocket = closeSocket;
|
|
44
|
+
exports.startSocket = startSocket;
|
|
45
|
+
exports.prepareAndStartSocket = prepareAndStartSocket;
|
|
46
|
+
const https_1 = __importDefault(require("https"));
|
|
47
|
+
const log = __importStar(require("../logger"));
|
|
48
|
+
const API_BASE = "https://api.dmdata.jp/v2";
|
|
49
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
50
|
+
const SOCKET_CLEANUP_MAX_RETRIES = 5;
|
|
51
|
+
const SOCKET_CLEANUP_RETRY_INTERVAL_MS = 500;
|
|
52
|
+
/** TLS ハンドシェイクを再利用するための keep-alive エージェント (遅延初期化) */
|
|
53
|
+
let keepAliveAgent = null;
|
|
54
|
+
function getKeepAliveAgent() {
|
|
55
|
+
if (keepAliveAgent == null) {
|
|
56
|
+
keepAliveAgent = new https_1.default.Agent({ keepAlive: true });
|
|
57
|
+
}
|
|
58
|
+
return keepAliveAgent;
|
|
59
|
+
}
|
|
60
|
+
/** dmdata.jp REST API の推奨方式に合わせて Basic 認証ヘッダーを構築 */
|
|
61
|
+
function buildAuthorizationHeader(apiKey) {
|
|
62
|
+
return `Basic ${Buffer.from(`${apiKey}:`).toString("base64")}`;
|
|
63
|
+
}
|
|
64
|
+
/** HTTPS リクエストを Promise でラップ */
|
|
65
|
+
function request(method, url, apiKey, body) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const parsed = new URL(url);
|
|
68
|
+
const options = {
|
|
69
|
+
hostname: parsed.hostname,
|
|
70
|
+
port: 443,
|
|
71
|
+
path: parsed.pathname + parsed.search,
|
|
72
|
+
method,
|
|
73
|
+
agent: getKeepAliveAgent(),
|
|
74
|
+
headers: {
|
|
75
|
+
Accept: "application/json",
|
|
76
|
+
Authorization: buildAuthorizationHeader(apiKey),
|
|
77
|
+
"Content-Type": "application/json",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const req = https_1.default.request(options, (res) => {
|
|
81
|
+
const statusCode = res.statusCode ?? 0;
|
|
82
|
+
let data = "";
|
|
83
|
+
res.on("data", (chunk) => (data += chunk));
|
|
84
|
+
res.on("end", () => {
|
|
85
|
+
// 204 No Content は成功(ボディなし)なのでそのまま返す
|
|
86
|
+
if (statusCode === 204) {
|
|
87
|
+
resolve({});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Content-Type チェック
|
|
91
|
+
const contentType = res.headers["content-type"] || "";
|
|
92
|
+
if (!contentType.includes("application/json")) {
|
|
93
|
+
reject(new Error(`${method} ${parsed.pathname}: 予期しない Content-Type: ${contentType} (status=${statusCode}, body=${data.slice(0, 200)})`));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const json = JSON.parse(data);
|
|
98
|
+
// HTTP ステータスコードの検証
|
|
99
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
100
|
+
const errMsg = typeof json === "object" && json != null && "error" in json
|
|
101
|
+
? json.error?.message || "Unknown error"
|
|
102
|
+
: data.slice(0, 200);
|
|
103
|
+
reject(new Error(`${method} ${parsed.pathname}: HTTP ${statusCode}: ${errMsg}`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
resolve(json);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
reject(new Error(`${method} ${parsed.pathname}: JSON パース失敗 (status=${statusCode}): ${data.slice(0, 200)}`));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
req.on("error", reject);
|
|
114
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
|
115
|
+
req.destroy(new Error(`Request timeout (${REQUEST_TIMEOUT_MS / 1000}s)`));
|
|
116
|
+
});
|
|
117
|
+
if (body) {
|
|
118
|
+
req.write(JSON.stringify(body));
|
|
119
|
+
}
|
|
120
|
+
req.end();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** 契約一覧を取得し、有効な区分を返す */
|
|
124
|
+
async function listContracts(apiKey) {
|
|
125
|
+
log.debug("GET /v2/contract");
|
|
126
|
+
const res = (await request("GET", `${API_BASE}/contract`, apiKey));
|
|
127
|
+
if (res.status === "error") {
|
|
128
|
+
throw new Error(`Contract List failed: ${res.error?.message} (code: ${res.error?.code})`);
|
|
129
|
+
}
|
|
130
|
+
const validClassifications = res.items
|
|
131
|
+
.filter((item) => item.isValid)
|
|
132
|
+
.map((item) => item.classification);
|
|
133
|
+
log.debug(`契約済み区分: ${validClassifications.join(", ") || "(なし)"}`);
|
|
134
|
+
return validClassifications;
|
|
135
|
+
}
|
|
136
|
+
/** 地震履歴を取得 */
|
|
137
|
+
async function listEarthquakes(apiKey, limit = 10) {
|
|
138
|
+
log.debug(`GET /v2/gd/earthquake?limit=${limit}`);
|
|
139
|
+
const res = (await request("GET", `${API_BASE}/gd/earthquake?limit=${limit}`, apiKey));
|
|
140
|
+
if (res.status === "error") {
|
|
141
|
+
throw new Error(`Earthquake List failed: ${res.error?.message} (code: ${res.error?.code})`);
|
|
142
|
+
}
|
|
143
|
+
return res;
|
|
144
|
+
}
|
|
145
|
+
/** 電文リストを取得 (GET /v2/telegram) */
|
|
146
|
+
async function listTelegrams(apiKey, type, limit = 1) {
|
|
147
|
+
const params = new URLSearchParams({
|
|
148
|
+
type,
|
|
149
|
+
limit: String(limit),
|
|
150
|
+
formatMode: "raw",
|
|
151
|
+
});
|
|
152
|
+
log.debug(`GET /v2/telegram?${params}`);
|
|
153
|
+
const res = (await request("GET", `${API_BASE}/telegram?${params}`, apiKey));
|
|
154
|
+
if (res.status === "error") {
|
|
155
|
+
throw new Error(`Telegram List failed: ${res.error?.message} (code: ${res.error?.code})`);
|
|
156
|
+
}
|
|
157
|
+
return res;
|
|
158
|
+
}
|
|
159
|
+
/** 既存のオープンソケットを取得 */
|
|
160
|
+
async function listSockets(apiKey) {
|
|
161
|
+
log.debug("GET /v2/socket?status=open");
|
|
162
|
+
const res = (await request("GET", `${API_BASE}/socket?status=open`, apiKey));
|
|
163
|
+
if (res.status === "error") {
|
|
164
|
+
throw new Error(`Socket List failed: ${res.error?.message} (code: ${res.error?.code})`);
|
|
165
|
+
}
|
|
166
|
+
return res;
|
|
167
|
+
}
|
|
168
|
+
/** 既存ソケットを閉じる */
|
|
169
|
+
async function closeSocket(apiKey, socketId) {
|
|
170
|
+
log.debug(`DELETE /v2/socket/${socketId}`);
|
|
171
|
+
const res = (await request("DELETE", `${API_BASE}/socket/${socketId}`, apiKey));
|
|
172
|
+
if (res.status === "error") {
|
|
173
|
+
log.warn(`Socket Close failed for id=${socketId}: ${res.error?.message}`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
log.info(`既存ソケット id=${socketId} をクローズしました`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Socket Start: WebSocket接続用チケットを取得 */
|
|
180
|
+
async function startSocket(config) {
|
|
181
|
+
const body = {
|
|
182
|
+
classifications: config.classifications,
|
|
183
|
+
test: config.testMode,
|
|
184
|
+
appName: config.appName,
|
|
185
|
+
formatMode: "raw",
|
|
186
|
+
};
|
|
187
|
+
log.debug(`POST /v2/socket body=${JSON.stringify(body)}`);
|
|
188
|
+
const res = (await request("POST", `${API_BASE}/socket`, config.apiKey, body));
|
|
189
|
+
if (res.status === "error") {
|
|
190
|
+
throw new Error(`Socket Start failed: ${res.error?.message} (code: ${res.error?.code})`);
|
|
191
|
+
}
|
|
192
|
+
return res;
|
|
193
|
+
}
|
|
194
|
+
/** サーバー側でソケット削除が反映されるのを待つ */
|
|
195
|
+
async function awaitSocketCleanup(apiKey, closedIds) {
|
|
196
|
+
for (let attempt = 1; attempt <= SOCKET_CLEANUP_MAX_RETRIES; attempt++) {
|
|
197
|
+
await new Promise((r) => setTimeout(r, SOCKET_CLEANUP_RETRY_INTERVAL_MS));
|
|
198
|
+
try {
|
|
199
|
+
const list = await listSockets(apiKey);
|
|
200
|
+
const stillOpen = list.items.filter((s) => s.status === "open" && closedIds.includes(s.id));
|
|
201
|
+
if (stillOpen.length === 0) {
|
|
202
|
+
log.debug(`ソケット削除の反映を確認 (${attempt} 回目)`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
log.debug(`ソケット削除待機中... 残存 ${stillOpen.length} 件 (${attempt}/${SOCKET_CLEANUP_MAX_RETRIES})`);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// リスト取得失敗は無視して次のリトライへ
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
log.warn("ソケット削除の反映を確認できませんでしたが、続行します");
|
|
212
|
+
}
|
|
213
|
+
/** 既存のオープン接続をすべて閉じてから Socket Start する */
|
|
214
|
+
async function prepareAndStartSocket(config, previousSocketId) {
|
|
215
|
+
/** クリーンアップで DELETE を送信したソケット ID */
|
|
216
|
+
const closedIds = [];
|
|
217
|
+
if (!config.keepExistingConnections) {
|
|
218
|
+
// 同一 appName のオープンソケットを閉じる(他デバイスのソケットは維持)
|
|
219
|
+
try {
|
|
220
|
+
const list = await listSockets(config.apiKey);
|
|
221
|
+
const allOpen = list.items.filter((s) => s.status === "open");
|
|
222
|
+
log.debug(`オープンソケット一覧 (${allOpen.length} 件): ${allOpen.map((s) => `id=${s.id},appName=${s.appName ?? "(null)"}`).join("; ") || "(なし)"}`);
|
|
223
|
+
log.debug(`自アプリ名: "${config.appName}", keepExisting=${config.keepExistingConnections}`);
|
|
224
|
+
const openSockets = allOpen.filter((s) => s.appName === config.appName);
|
|
225
|
+
if (openSockets.length > 0) {
|
|
226
|
+
const skipped = allOpen.filter((s) => s.appName !== config.appName).length;
|
|
227
|
+
if (skipped > 0) {
|
|
228
|
+
log.info(`他アプリの ${skipped} 件のソケットは維持します`);
|
|
229
|
+
}
|
|
230
|
+
await Promise.allSettled(openSockets.map((sock) => closeSocket(config.apiKey, sock.id)));
|
|
231
|
+
closedIds.push(...openSockets.map((s) => s.id));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
log.warn(`既存ソケット確認中にエラー: ${err instanceof Error ? err.message : err}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (previousSocketId != null) {
|
|
239
|
+
// 再接続: 自分の旧接続だけを閉じる (サーバー側で既に閉じられている場合は 404 が返る)
|
|
240
|
+
try {
|
|
241
|
+
await closeSocket(config.apiKey, previousSocketId);
|
|
242
|
+
closedIds.push(previousSocketId);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
246
|
+
if (errMsg.includes("404")) {
|
|
247
|
+
log.debug(`旧ソケット(id=${previousSocketId})は既にサーバー側で閉じられています`);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
log.warn(`旧ソケット(id=${previousSocketId})のクローズに失敗: ${errMsg}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// 初回起動: 前回セッションの残留ソケットをクリーンアップ
|
|
256
|
+
// appName でフィルタリングし、他デバイスのソケットを誤って閉じないようにする
|
|
257
|
+
try {
|
|
258
|
+
const list = await listSockets(config.apiKey);
|
|
259
|
+
const allOpen = list.items.filter((s) => s.status === "open");
|
|
260
|
+
log.debug(`オープンソケット一覧 (${allOpen.length} 件): ${allOpen.map((s) => `id=${s.id},appName=${s.appName ?? "(null)"}`).join("; ") || "(なし)"}`);
|
|
261
|
+
log.debug(`自アプリ名: "${config.appName}", keepExisting=${config.keepExistingConnections}`);
|
|
262
|
+
const openSockets = allOpen.filter((s) => s.appName === config.appName);
|
|
263
|
+
if (openSockets.length > 0) {
|
|
264
|
+
const skipped = allOpen.filter((s) => s.appName !== config.appName).length;
|
|
265
|
+
log.info(`前回セッションの残留ソケットを ${openSockets.length} 件クローズします` +
|
|
266
|
+
(skipped > 0 ? ` (他アプリの ${skipped} 件は維持)` : ""));
|
|
267
|
+
await Promise.allSettled(openSockets.map((sock) => closeSocket(config.apiKey, sock.id)));
|
|
268
|
+
closedIds.push(...openSockets.map((s) => s.id));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
log.warn(`残留ソケット確認中にエラー: ${err instanceof Error ? err.message : err}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// ソケットを閉じた場合、サーバー側で削除が反映されるのを待ってから新規作成する
|
|
276
|
+
// (反映前に POST /v2/socket すると同時接続上限を超過し、他デバイスが切断される)
|
|
277
|
+
if (closedIds.length > 0) {
|
|
278
|
+
await awaitSocketCleanup(config.apiKey, closedIds);
|
|
279
|
+
}
|
|
280
|
+
return startSocket(config);
|
|
281
|
+
}
|