iobroker.parcelapp 0.4.2 → 0.4.4
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/README.md +7 -7
- package/build/lib/parcel-client.js +41 -4
- package/build/lib/parcel-client.js.map +3 -3
- package/build/lib/state-manager.js +13 -1
- package/build/lib/state-manager.js.map +2 -2
- package/build/main.js +42 -8
- package/build/main.js.map +2 -2
- package/io-package.json +27 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://ko-fi.com/krobipd)
|
|
10
10
|
[](https://paypal.me/krobipd)
|
|
11
11
|
|
|
12
|
-
<img src="https://
|
|
12
|
+
<img src="https://cdn.jsdelivr.net/gh/krobipd/ioBroker.parcelapp@main/admin/parcelapp.svg" width="100" />
|
|
13
13
|
|
|
14
14
|
ioBroker adapter for the [parcel.app](https://parcelapp.net) API. Supports all carriers that parcel.app tracks.
|
|
15
15
|
|
|
@@ -122,6 +122,12 @@ The delivery is added to your parcel.app account and immediately appears in ioBr
|
|
|
122
122
|
Placeholder for the next version (at the beginning of the line):
|
|
123
123
|
### **WORK IN PROGRESS**
|
|
124
124
|
-->
|
|
125
|
+
### 0.4.4 (2026-05-13)
|
|
126
|
+
- Adapter shuts down cleanly even if the "Test Connection" button was still running — the test request is now aborted at unload along with regular polling.
|
|
127
|
+
|
|
128
|
+
### 0.4.3 (2026-05-13)
|
|
129
|
+
- Debug log now traces previously silent paths: HTTPS request lifecycle, carrier-list fetch outcome, per-delivery updates, admin-message handling and lifecycle anchors. Default log unchanged.
|
|
130
|
+
|
|
125
131
|
### 0.4.2 (2026-05-10)
|
|
126
132
|
|
|
127
133
|
- Adapter shuts down cleanly even if parcel.app is slow — pending requests are aborted instead of hanging until kill.
|
|
@@ -138,12 +144,6 @@ The delivery is added to your parcel.app account and immediately appears in ioBr
|
|
|
138
144
|
- State names now follow your ioBroker system language (11 languages).
|
|
139
145
|
- Minimum requirements: Node.js 22 and ioBroker Admin 7.8.23.
|
|
140
146
|
|
|
141
|
-
### 0.3.2 (2026-05-01)
|
|
142
|
-
- Documentation: rewrote release notes for v0.2.14–v0.3.1 in user-friendly style across all languages.
|
|
143
|
-
|
|
144
|
-
### 0.3.1 (2026-05-01)
|
|
145
|
-
- Documentation cleanup. No code changes.
|
|
146
|
-
|
|
147
147
|
Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).
|
|
148
148
|
|
|
149
149
|
## Support
|
|
@@ -46,14 +46,22 @@ class ParcelClient {
|
|
|
46
46
|
* 4-second kill deadline.
|
|
47
47
|
*/
|
|
48
48
|
inflight = /* @__PURE__ */ new Set();
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/** v0.4.3: optional logger for the HTTPS-layer trace. See {@link ParcelClientLogger}. */
|
|
50
|
+
log;
|
|
51
|
+
/**
|
|
52
|
+
* @param apiKey The parcel.app API key
|
|
53
|
+
* @param log Optional adapter logger for HTTPS-layer trace (v0.4.3)
|
|
54
|
+
*/
|
|
55
|
+
constructor(apiKey, log) {
|
|
51
56
|
this.apiKey = apiKey;
|
|
57
|
+
this.log = log;
|
|
52
58
|
}
|
|
53
59
|
/**
|
|
54
60
|
* v0.4.2 (P1): abort every in-flight HTTPS request. Idempotent.
|
|
55
61
|
*/
|
|
56
62
|
cancelAll() {
|
|
63
|
+
var _a;
|
|
64
|
+
(_a = this.log) == null ? void 0 : _a.debug(`cancelAll: aborting ${this.inflight.size} inflight requests`);
|
|
57
65
|
for (const ctrl of this.inflight) {
|
|
58
66
|
ctrl.abort();
|
|
59
67
|
}
|
|
@@ -64,8 +72,10 @@ class ParcelClient {
|
|
|
64
72
|
* @param filterMode Filter active or recent deliveries
|
|
65
73
|
*/
|
|
66
74
|
async getDeliveries(filterMode = "active") {
|
|
75
|
+
var _a, _b;
|
|
67
76
|
const response = await this.request("GET", `/deliveries/?filter_mode=${filterMode}`, true);
|
|
68
77
|
if (!response || typeof response !== "object") {
|
|
78
|
+
(_a = this.log) == null ? void 0 : _a.debug(`API drift: malformed response (got ${typeof response})`);
|
|
69
79
|
const err = new Error("API error: malformed response");
|
|
70
80
|
err.code = "API_ERROR";
|
|
71
81
|
throw err;
|
|
@@ -74,6 +84,7 @@ class ParcelClient {
|
|
|
74
84
|
const rawCode = typeof response.error_code === "string" ? response.error_code : "";
|
|
75
85
|
const rawMsg = typeof response.error_message === "string" ? response.error_message : "";
|
|
76
86
|
const code = rawCode || rawMsg || "UNKNOWN";
|
|
87
|
+
(_b = this.log) == null ? void 0 : _b.debug(`API drift: success=false, code='${code}', msg='${rawMsg}'`);
|
|
77
88
|
const err = new Error(`API error: ${rawMsg || code}`);
|
|
78
89
|
err.code = rawCode === "INVALID_API_KEY" ? "INVALID_API_KEY" : "API_ERROR";
|
|
79
90
|
throw err;
|
|
@@ -90,6 +101,7 @@ class ParcelClient {
|
|
|
90
101
|
}
|
|
91
102
|
/** Get carrier names (cached after first call) */
|
|
92
103
|
async getCarrierNames() {
|
|
104
|
+
var _a, _b, _c;
|
|
93
105
|
if (this.carrierCache) {
|
|
94
106
|
return this.carrierCache;
|
|
95
107
|
}
|
|
@@ -97,10 +109,16 @@ class ParcelClient {
|
|
|
97
109
|
const raw = await this.request("GET", "/supported_carriers.json", false);
|
|
98
110
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
99
111
|
this.carrierCache = raw;
|
|
112
|
+
(_a = this.log) == null ? void 0 : _a.debug(`carriers: fetched ${Object.keys(this.carrierCache).length} entries`);
|
|
100
113
|
} else {
|
|
114
|
+
(_b = this.log) == null ? void 0 : _b.debug(
|
|
115
|
+
`carriers: drift (got ${Array.isArray(raw) ? "array" : typeof raw}, expected object), kept empty`
|
|
116
|
+
);
|
|
101
117
|
return {};
|
|
102
118
|
}
|
|
103
|
-
} catch {
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
(_c = this.log) == null ? void 0 : _c.debug(`carriers: fetch failed (kept empty, will retry): ${msg}`);
|
|
104
122
|
return {};
|
|
105
123
|
}
|
|
106
124
|
return this.carrierCache;
|
|
@@ -111,7 +129,9 @@ class ParcelClient {
|
|
|
111
129
|
* @param carrierCode The carrier code from API
|
|
112
130
|
*/
|
|
113
131
|
async getCarrierName(carrierCode) {
|
|
132
|
+
var _a;
|
|
114
133
|
if (typeof carrierCode !== "string" || carrierCode.length === 0) {
|
|
134
|
+
(_a = this.log) == null ? void 0 : _a.debug(`getCarrierName: non-string code (got ${typeof carrierCode}), returning UNKNOWN`);
|
|
115
135
|
return "UNKNOWN";
|
|
116
136
|
}
|
|
117
137
|
const carriers = await this.getCarrierNames();
|
|
@@ -140,11 +160,16 @@ class ParcelClient {
|
|
|
140
160
|
* @param body Optional request body
|
|
141
161
|
*/
|
|
142
162
|
request(method, path, authenticated, body) {
|
|
163
|
+
var _a;
|
|
164
|
+
const startedAt = Date.now();
|
|
165
|
+
(_a = this.log) == null ? void 0 : _a.debug(`HTTP ${method} ${path}`);
|
|
143
166
|
return new Promise((resolve, reject) => {
|
|
167
|
+
var _a2;
|
|
144
168
|
let url;
|
|
145
169
|
try {
|
|
146
170
|
url = new URL(`${API_BASE}${path}`);
|
|
147
171
|
} catch {
|
|
172
|
+
(_a2 = this.log) == null ? void 0 : _a2.debug(`HTTP invalid URL: ${API_BASE}${path}`);
|
|
148
173
|
const err = new Error(`Invalid URL: ${API_BASE}${path}`);
|
|
149
174
|
err.code = "INVALID_URL";
|
|
150
175
|
reject(err);
|
|
@@ -179,18 +204,21 @@ class ParcelClient {
|
|
|
179
204
|
reject(err);
|
|
180
205
|
});
|
|
181
206
|
res.on("data", (chunk) => {
|
|
207
|
+
var _a3;
|
|
182
208
|
if (oversized) {
|
|
183
209
|
return;
|
|
184
210
|
}
|
|
185
211
|
bodyBytes += chunk.length;
|
|
186
212
|
if (bodyBytes > MAX_BODY_BYTES) {
|
|
187
213
|
oversized = true;
|
|
214
|
+
(_a3 = this.log) == null ? void 0 : _a3.debug(`HTTP body oversized ${path}: dropping at ${bodyBytes}B`);
|
|
188
215
|
req.destroy(new Error(`Response body exceeds ${MAX_BODY_BYTES} bytes`));
|
|
189
216
|
return;
|
|
190
217
|
}
|
|
191
218
|
chunks.push(chunk);
|
|
192
219
|
});
|
|
193
220
|
res.on("end", () => {
|
|
221
|
+
var _a3, _b, _c, _d;
|
|
194
222
|
cleanup();
|
|
195
223
|
if (oversized) {
|
|
196
224
|
const err = new Error("Response body too large");
|
|
@@ -205,6 +233,7 @@ class ParcelClient {
|
|
|
205
233
|
const err2 = new Error("Rate limit exceeded");
|
|
206
234
|
err2.code = "RATE_LIMITED";
|
|
207
235
|
err2.retryAfterSeconds = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(24 * 3600, retryAfter) : 5 * 60;
|
|
236
|
+
(_a3 = this.log) == null ? void 0 : _a3.debug(`HTTP 429 ${path} \u2192 retry-after=${err2.retryAfterSeconds}s`);
|
|
208
237
|
reject(err2);
|
|
209
238
|
return;
|
|
210
239
|
}
|
|
@@ -216,12 +245,16 @@ class ParcelClient {
|
|
|
216
245
|
} else {
|
|
217
246
|
err.code = "HTTP_ERROR";
|
|
218
247
|
}
|
|
248
|
+
(_b = this.log) == null ? void 0 : _b.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} ${err.code} (body=${raw.substring(0, 200)})`);
|
|
219
249
|
reject(err);
|
|
220
250
|
return;
|
|
221
251
|
}
|
|
222
252
|
try {
|
|
223
|
-
|
|
253
|
+
const parsed = JSON.parse(raw);
|
|
254
|
+
(_c = this.log) == null ? void 0 : _c.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} (${Date.now() - startedAt}ms, ${bodyBytes}B)`);
|
|
255
|
+
resolve(parsed);
|
|
224
256
|
} catch {
|
|
257
|
+
(_d = this.log) == null ? void 0 : _d.debug(`HTTP JSON parse fail ${path}: ${raw.substring(0, 200)}`);
|
|
225
258
|
reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));
|
|
226
259
|
}
|
|
227
260
|
});
|
|
@@ -230,12 +263,16 @@ class ParcelClient {
|
|
|
230
263
|
req.destroy(new Error("Request aborted"));
|
|
231
264
|
});
|
|
232
265
|
req.on("timeout", () => {
|
|
266
|
+
var _a3;
|
|
233
267
|
req.destroy();
|
|
234
268
|
cleanup();
|
|
269
|
+
(_a3 = this.log) == null ? void 0 : _a3.debug(`HTTP timeout ${method} ${path} (${Date.now() - startedAt}ms)`);
|
|
235
270
|
reject(new Error("Request timeout"));
|
|
236
271
|
});
|
|
237
272
|
req.on("error", (err) => {
|
|
273
|
+
var _a3;
|
|
238
274
|
cleanup();
|
|
275
|
+
(_a3 = this.log) == null ? void 0 : _a3.debug(`HTTP error ${method} ${path} (${Date.now() - startedAt}ms): ${err.message}`);
|
|
239
276
|
reject(err);
|
|
240
277
|
});
|
|
241
278
|
if (body) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/parcel-client.ts"],
|
|
4
|
-
"sourcesContent": ["import * as https from \"node:https\";\nimport { isTrueish } from \"./coerce\";\nimport type { ParcelApiResponse, ParcelDelivery, AddDeliveryRequest, AddDeliveryResponse, CarrierMap } from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\nconst REQUEST_TIMEOUT = 15_000;\n/**\n * v0.4.2 (P9): hard cap on response body size. parcel.app deliveries lists\n * are tiny (~1 kB per package, max ~50 packages = 50 kB), so a 1 MiB cap is\n * 20\u00D7 the realistic max while still defending against a runaway response.\n */\nconst MAX_BODY_BYTES = 1 << 20; // 1 MiB\n\n/** HTTP client for the parcel.app API */\nexport class ParcelClient {\n private apiKey: string;\n private carrierCache: CarrierMap | null = null;\n /**\n * v0.4.2 (P1): per-request AbortController. `cancelAll()` aborts every\n * pending HTTPS request \u2014 called from the adapter's `onUnload` so a slow\n * parcel.app endpoint can't keep the adapter alive past js-controller's\n * 4-second kill deadline.\n */\n private readonly inflight = new Set<AbortController>();\n\n /** @param apiKey The parcel.app API key */\n constructor(apiKey: string) {\n this.apiKey = apiKey;\n }\n\n /**\n * v0.4.2 (P1): abort every in-flight HTTPS request. Idempotent.\n */\n cancelAll(): void {\n for (const ctrl of this.inflight) {\n ctrl.abort();\n }\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * @param filterMode Filter active or recent deliveries\n */\n async getDeliveries(filterMode: \"active\" | \"recent\" = \"active\"): Promise<ParcelDelivery[]> {\n const response = await this.request<ParcelApiResponse>(\"GET\", `/deliveries/?filter_mode=${filterMode}`, true);\n\n // API-drift guard: response may be null or a non-object\n if (!response || typeof response !== \"object\") {\n const err = new Error(\"API error: malformed response\") as Error & {\n code: string;\n };\n err.code = \"API_ERROR\";\n throw err;\n }\n\n if (!isTrueish(response.success)) {\n const rawCode = typeof response.error_code === \"string\" ? response.error_code : \"\";\n const rawMsg = typeof response.error_message === \"string\" ? response.error_message : \"\";\n const code = rawCode || rawMsg || \"UNKNOWN\";\n const err = new Error(`API error: ${rawMsg || code}`) as Error & {\n code: string;\n };\n err.code = rawCode === \"INVALID_API_KEY\" ? \"INVALID_API_KEY\" : \"API_ERROR\";\n throw err;\n }\n\n // API-drift guard: deliveries must be an array\n return Array.isArray(response.deliveries) ? response.deliveries : [];\n }\n\n /**\n * Add a new delivery to parcel.app.\n *\n * @param delivery The delivery to add\n */\n async addDelivery(delivery: AddDeliveryRequest): Promise<AddDeliveryResponse> {\n return this.request<AddDeliveryResponse>(\"POST\", \"/add-delivery/\", true, delivery);\n }\n\n /** Get carrier names (cached after first call) */\n async getCarrierNames(): Promise<CarrierMap> {\n if (this.carrierCache) {\n return this.carrierCache;\n }\n\n try {\n const raw = await this.request<unknown>(\"GET\", \"/supported_carriers.json\", false);\n // API-drift guard: must be a plain object (not null, array, or primitive)\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n this.carrierCache = raw as CarrierMap;\n } else {\n return {};\n }\n } catch {\n // Return empty map but don't cache it \u2014 allow retry next time\n return {};\n }\n\n return this.carrierCache;\n }\n\n /**\n * Resolve a carrier code to a display name.\n *\n * @param carrierCode The carrier code from API\n */\n async getCarrierName(carrierCode: unknown): Promise<string> {\n // API-drift guard: non-string codes fall back to \"UNKNOWN\"\n if (typeof carrierCode !== \"string\" || carrierCode.length === 0) {\n return \"UNKNOWN\";\n }\n const carriers = await this.getCarrierNames();\n const mapped = carriers[carrierCode];\n return typeof mapped === \"string\" && mapped.length > 0 ? mapped : carrierCode.toUpperCase();\n }\n\n /** Test if the API key is valid */\n async testConnection(): Promise<{ success: boolean; message: string }> {\n try {\n await this.getDeliveries(\"active\");\n return { success: true, message: \"Connection successful\" };\n } catch (err) {\n const error = err as Error & { code?: string };\n if (error.code === \"INVALID_API_KEY\") {\n return { success: false, message: \"Invalid API key\" };\n }\n return { success: false, message: error.message };\n }\n }\n\n /**\n * Execute an HTTP request against the parcel.app API.\n *\n * @param method HTTP method\n * @param path API path\n * @param authenticated Whether to send the API key\n * @param body Optional request body\n */\n private request<T>(method: string, path: string, authenticated: boolean, body?: unknown): Promise<T> {\n return new Promise((resolve, reject) => {\n // v0.4.2 (E3): URL-shape validation defensive \u2014 paths are hardcoded\n // upstream but a future caller could pass garbage; surface a clear\n // error class instead of a TypeError thrown sync from the executor.\n let url: URL;\n try {\n url = new URL(`${API_BASE}${path}`);\n } catch {\n const err = new Error(`Invalid URL: ${API_BASE}${path}`) as Error & { code: string };\n err.code = \"INVALID_URL\";\n reject(err);\n return;\n }\n\n const headers: Record<string, string> = {};\n if (authenticated) {\n headers[\"api-key\"] = this.apiKey;\n }\n if (body) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n const options: https.RequestOptions = {\n hostname: url.hostname,\n port: url.port || 443,\n path: url.pathname + url.search,\n method,\n headers,\n timeout: REQUEST_TIMEOUT,\n };\n\n // v0.4.2 (P1): per-request AbortController. `cancelAll()` (called\n // from `onUnload`) aborts everything pending without waiting for\n // the configured timeout.\n const ctrl = new AbortController();\n this.inflight.add(ctrl);\n const cleanup = (): void => {\n this.inflight.delete(ctrl);\n };\n\n const req = https.request(options, res => {\n const chunks: Buffer[] = [];\n let bodyBytes = 0;\n let oversized = false;\n\n res.on(\"error\", err => {\n cleanup();\n reject(err);\n });\n res.on(\"data\", (chunk: Buffer) => {\n if (oversized) {\n return;\n }\n bodyBytes += chunk.length;\n // v0.4.2 (P9): drop the connection on oversized responses so a\n // compromised or misconfigured endpoint can't OOM the adapter.\n if (bodyBytes > MAX_BODY_BYTES) {\n oversized = true;\n req.destroy(new Error(`Response body exceeds ${MAX_BODY_BYTES} bytes`));\n return;\n }\n chunks.push(chunk);\n });\n res.on(\"end\", () => {\n cleanup();\n if (oversized) {\n const err = new Error(\"Response body too large\") as Error & { code: string };\n err.code = \"BODY_TOO_LARGE\";\n reject(err);\n return;\n }\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n\n if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {\n if (res.statusCode === 429) {\n // v0.4.2 (P6): clamp Retry-After parser. Bogus values (0,\n // negative, NaN) used to fall through `>0` and default to\n // 5 min \u2014 keep that, but also reject infinity/extreme.\n const retryAfter = parseInt(res.headers[\"retry-after\"] || \"\", 10);\n const err = new Error(\"Rate limit exceeded\") as Error & {\n code: string;\n retryAfterSeconds: number;\n };\n err.code = \"RATE_LIMITED\";\n err.retryAfterSeconds =\n Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(24 * 3600, retryAfter) : 5 * 60;\n reject(err);\n return;\n }\n // v0.4.2 (P3): split 401 (invalid key) from 403 (permission /\n // no premium). Adapter treats them differently \u2014 INVALID_API_KEY\n // says \"fix the key\", FORBIDDEN says \"fix the account\".\n const err = new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`) as Error & { code: string };\n if (res.statusCode === 401) {\n err.code = \"INVALID_API_KEY\";\n } else if (res.statusCode === 403) {\n err.code = \"FORBIDDEN\";\n } else {\n err.code = \"HTTP_ERROR\";\n }\n reject(err);\n return;\n }\n\n try {\n resolve(JSON.parse(raw) as T);\n } catch {\n reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));\n }\n });\n });\n\n ctrl.signal.addEventListener(\"abort\", () => {\n req.destroy(new Error(\"Request aborted\"));\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n cleanup();\n reject(new Error(\"Request timeout\"));\n });\n\n req.on(\"error\", err => {\n cleanup();\n reject(err);\n });\n\n if (body) {\n req.write(JSON.stringify(body));\n }\n req.end();\n });\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAAuB;AACvB,oBAA0B;AAG1B,MAAM,WAAW;AACjB,MAAM,kBAAkB;
|
|
6
|
-
"names": ["err"]
|
|
4
|
+
"sourcesContent": ["import * as https from \"node:https\";\nimport { isTrueish } from \"./coerce\";\nimport type { ParcelApiResponse, ParcelDelivery, AddDeliveryRequest, AddDeliveryResponse, CarrierMap } from \"./types\";\n\nconst API_BASE = \"https://api.parcel.app/external\";\nconst REQUEST_TIMEOUT = 15_000;\n\n/**\n * v0.4.3: optional logger injected by the adapter so the HTTPS client can\n * trace its own request/response lifecycle. When omitted (e.g. in tests),\n * every `this.log?.debug(...)` call is a no-op \u2014 keeps the bare-`apiKey`\n * constructor signature backward-compatible.\n */\nexport interface ParcelClientLogger {\n /** Adapter debug log. Called at most once per request/response decision. */\n debug(message: string): void;\n}\n/**\n * v0.4.2 (P9): hard cap on response body size. parcel.app deliveries lists\n * are tiny (~1 kB per package, max ~50 packages = 50 kB), so a 1 MiB cap is\n * 20\u00D7 the realistic max while still defending against a runaway response.\n */\nconst MAX_BODY_BYTES = 1 << 20; // 1 MiB\n\n/** HTTP client for the parcel.app API */\nexport class ParcelClient {\n private apiKey: string;\n private carrierCache: CarrierMap | null = null;\n /**\n * v0.4.2 (P1): per-request AbortController. `cancelAll()` aborts every\n * pending HTTPS request \u2014 called from the adapter's `onUnload` so a slow\n * parcel.app endpoint can't keep the adapter alive past js-controller's\n * 4-second kill deadline.\n */\n private readonly inflight = new Set<AbortController>();\n /** v0.4.3: optional logger for the HTTPS-layer trace. See {@link ParcelClientLogger}. */\n private readonly log?: ParcelClientLogger;\n\n /**\n * @param apiKey The parcel.app API key\n * @param log Optional adapter logger for HTTPS-layer trace (v0.4.3)\n */\n constructor(apiKey: string, log?: ParcelClientLogger) {\n this.apiKey = apiKey;\n this.log = log;\n }\n\n /**\n * v0.4.2 (P1): abort every in-flight HTTPS request. Idempotent.\n */\n cancelAll(): void {\n // v0.4.3 (A12): trace the shutdown anchor so the adapter log shows\n // exactly how many HTTPS calls were aborted at unload.\n this.log?.debug(`cancelAll: aborting ${this.inflight.size} inflight requests`);\n for (const ctrl of this.inflight) {\n ctrl.abort();\n }\n }\n\n /**\n * Fetch deliveries from parcel.app.\n *\n * @param filterMode Filter active or recent deliveries\n */\n async getDeliveries(filterMode: \"active\" | \"recent\" = \"active\"): Promise<ParcelDelivery[]> {\n const response = await this.request<ParcelApiResponse>(\"GET\", `/deliveries/?filter_mode=${filterMode}`, true);\n\n // API-drift guard: response may be null or a non-object\n if (!response || typeof response !== \"object\") {\n // v0.4.3 (A11a): trace malformed-response drift before throwing.\n this.log?.debug(`API drift: malformed response (got ${typeof response})`);\n const err = new Error(\"API error: malformed response\") as Error & {\n code: string;\n };\n err.code = \"API_ERROR\";\n throw err;\n }\n\n if (!isTrueish(response.success)) {\n const rawCode = typeof response.error_code === \"string\" ? response.error_code : \"\";\n const rawMsg = typeof response.error_message === \"string\" ? response.error_message : \"\";\n const code = rawCode || rawMsg || \"UNKNOWN\";\n // v0.4.3 (A11b): trace API-side error before throwing.\n this.log?.debug(`API drift: success=false, code='${code}', msg='${rawMsg}'`);\n const err = new Error(`API error: ${rawMsg || code}`) as Error & {\n code: string;\n };\n err.code = rawCode === \"INVALID_API_KEY\" ? \"INVALID_API_KEY\" : \"API_ERROR\";\n throw err;\n }\n\n // API-drift guard: deliveries must be an array\n return Array.isArray(response.deliveries) ? response.deliveries : [];\n }\n\n /**\n * Add a new delivery to parcel.app.\n *\n * @param delivery The delivery to add\n */\n async addDelivery(delivery: AddDeliveryRequest): Promise<AddDeliveryResponse> {\n return this.request<AddDeliveryResponse>(\"POST\", \"/add-delivery/\", true, delivery);\n }\n\n /** Get carrier names (cached after first call) */\n async getCarrierNames(): Promise<CarrierMap> {\n if (this.carrierCache) {\n return this.carrierCache;\n }\n\n try {\n const raw = await this.request<unknown>(\"GET\", \"/supported_carriers.json\", false);\n // API-drift guard: must be a plain object (not null, array, or primitive)\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n this.carrierCache = raw as CarrierMap;\n // v0.4.3 (D1): trace the one-time cache fill so a successful warm-up\n // is visible in the debug log (happens once per adapter restart).\n this.log?.debug(`carriers: fetched ${Object.keys(this.carrierCache).length} entries`);\n } else {\n // v0.4.3 (D3): non-object drift \u2014 supported_carriers.json returned\n // something that isn't an object. Empty map is returned, NOT cached.\n this.log?.debug(\n `carriers: drift (got ${Array.isArray(raw) ? \"array\" : typeof raw}, expected object), kept empty`,\n );\n return {};\n }\n } catch (err) {\n // v0.4.3 (D2): trace the fetch-fail so the empty-map fallback isn't\n // silent. NOT cached \u2014 next poll retries; the trace then shows the\n // retry, too. Without this the user sees carrier codes instead of\n // names with no log entry explaining why.\n const msg = err instanceof Error ? err.message : String(err);\n this.log?.debug(`carriers: fetch failed (kept empty, will retry): ${msg}`);\n // Return empty map but don't cache it \u2014 allow retry next time\n return {};\n }\n\n return this.carrierCache;\n }\n\n /**\n * Resolve a carrier code to a display name.\n *\n * @param carrierCode The carrier code from API\n */\n async getCarrierName(carrierCode: unknown): Promise<string> {\n // API-drift guard: non-string codes fall back to \"UNKNOWN\"\n if (typeof carrierCode !== \"string\" || carrierCode.length === 0) {\n // v0.4.3 (D4): trace non-string code drift. Helps diagnose \"all my\n // packages show UNKNOWN carrier\" reports.\n this.log?.debug(`getCarrierName: non-string code (got ${typeof carrierCode}), returning UNKNOWN`);\n return \"UNKNOWN\";\n }\n const carriers = await this.getCarrierNames();\n const mapped = carriers[carrierCode];\n return typeof mapped === \"string\" && mapped.length > 0 ? mapped : carrierCode.toUpperCase();\n }\n\n /** Test if the API key is valid */\n async testConnection(): Promise<{ success: boolean; message: string }> {\n try {\n await this.getDeliveries(\"active\");\n return { success: true, message: \"Connection successful\" };\n } catch (err) {\n const error = err as Error & { code?: string };\n if (error.code === \"INVALID_API_KEY\") {\n return { success: false, message: \"Invalid API key\" };\n }\n return { success: false, message: error.message };\n }\n }\n\n /**\n * Execute an HTTP request against the parcel.app API.\n *\n * @param method HTTP method\n * @param path API path\n * @param authenticated Whether to send the API key\n * @param body Optional request body\n */\n private request<T>(method: string, path: string, authenticated: boolean, body?: unknown): Promise<T> {\n // v0.4.3 (A0): start timestamp for elapsed-ms in the success/timeout/error\n // log lines. One LOC, no behavior change.\n const startedAt = Date.now();\n // v0.4.3 (A1): trace request entry. ~144 calls/day at the default 10-min\n // poll interval \u2014 acceptable at debug.\n this.log?.debug(`HTTP ${method} ${path}`);\n return new Promise((resolve, reject) => {\n // v0.4.2 (E3): URL-shape validation defensive \u2014 paths are hardcoded\n // upstream but a future caller could pass garbage; surface a clear\n // error class instead of a TypeError thrown sync from the executor.\n let url: URL;\n try {\n url = new URL(`${API_BASE}${path}`);\n } catch {\n // v0.4.3 (A10): trace invalid-URL drift before throwing.\n this.log?.debug(`HTTP invalid URL: ${API_BASE}${path}`);\n const err = new Error(`Invalid URL: ${API_BASE}${path}`) as Error & { code: string };\n err.code = \"INVALID_URL\";\n reject(err);\n return;\n }\n\n const headers: Record<string, string> = {};\n if (authenticated) {\n headers[\"api-key\"] = this.apiKey;\n }\n if (body) {\n headers[\"Content-Type\"] = \"application/json\";\n }\n\n const options: https.RequestOptions = {\n hostname: url.hostname,\n port: url.port || 443,\n path: url.pathname + url.search,\n method,\n headers,\n timeout: REQUEST_TIMEOUT,\n };\n\n // v0.4.2 (P1): per-request AbortController. `cancelAll()` (called\n // from `onUnload`) aborts everything pending without waiting for\n // the configured timeout.\n const ctrl = new AbortController();\n this.inflight.add(ctrl);\n const cleanup = (): void => {\n this.inflight.delete(ctrl);\n };\n\n const req = https.request(options, res => {\n const chunks: Buffer[] = [];\n let bodyBytes = 0;\n let oversized = false;\n\n res.on(\"error\", err => {\n cleanup();\n reject(err);\n });\n res.on(\"data\", (chunk: Buffer) => {\n if (oversized) {\n return;\n }\n bodyBytes += chunk.length;\n // v0.4.2 (P9): drop the connection on oversized responses so a\n // compromised or misconfigured endpoint can't OOM the adapter.\n if (bodyBytes > MAX_BODY_BYTES) {\n oversized = true;\n // v0.4.3 (A9): trace the oversize-drop before destroying.\n this.log?.debug(`HTTP body oversized ${path}: dropping at ${bodyBytes}B`);\n req.destroy(new Error(`Response body exceeds ${MAX_BODY_BYTES} bytes`));\n return;\n }\n chunks.push(chunk);\n });\n res.on(\"end\", () => {\n cleanup();\n if (oversized) {\n const err = new Error(\"Response body too large\") as Error & { code: string };\n err.code = \"BODY_TOO_LARGE\";\n reject(err);\n return;\n }\n const raw = Buffer.concat(chunks).toString(\"utf-8\");\n\n if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {\n if (res.statusCode === 429) {\n // v0.4.2 (P6): clamp Retry-After parser. Bogus values (0,\n // negative, NaN) used to fall through `>0` and default to\n // 5 min \u2014 keep that, but also reject infinity/extreme.\n const retryAfter = parseInt(res.headers[\"retry-after\"] || \"\", 10);\n const err = new Error(\"Rate limit exceeded\") as Error & {\n code: string;\n retryAfterSeconds: number;\n };\n err.code = \"RATE_LIMITED\";\n err.retryAfterSeconds =\n Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(24 * 3600, retryAfter) : 5 * 60;\n // v0.4.3 (A4): trace 429 with the parsed retry-after.\n this.log?.debug(`HTTP 429 ${path} \u2192 retry-after=${err.retryAfterSeconds}s`);\n reject(err);\n return;\n }\n // v0.4.2 (P3): split 401 (invalid key) from 403 (permission /\n // no premium). Adapter treats them differently \u2014 INVALID_API_KEY\n // says \"fix the key\", FORBIDDEN says \"fix the account\".\n const err = new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`) as Error & { code: string };\n if (res.statusCode === 401) {\n err.code = \"INVALID_API_KEY\";\n } else if (res.statusCode === 403) {\n err.code = \"FORBIDDEN\";\n } else {\n err.code = \"HTTP_ERROR\";\n }\n // v0.4.3 (A3): trace 4xx/5xx with body-snippet for diagnosis.\n this.log?.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} ${err.code} (body=${raw.substring(0, 200)})`);\n reject(err);\n return;\n }\n\n try {\n const parsed = JSON.parse(raw) as T;\n // v0.4.3 (A2): trace successful response with elapsed-ms + bytes.\n this.log?.debug(`HTTP ${method} ${path} \u2192 ${res.statusCode} (${Date.now() - startedAt}ms, ${bodyBytes}B)`);\n resolve(parsed);\n } catch {\n // v0.4.3 (A8): trace JSON parse-fail with snippet.\n this.log?.debug(`HTTP JSON parse fail ${path}: ${raw.substring(0, 200)}`);\n reject(new Error(`JSON parse error: ${raw.substring(0, 200)}`));\n }\n });\n });\n\n ctrl.signal.addEventListener(\"abort\", () => {\n // v0.4.3: A6 deliberately omitted \u2014 `req.destroy(Error)` propagates\n // through `req.on(\"error\")` below where A7 already logs it.\n req.destroy(new Error(\"Request aborted\"));\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n cleanup();\n // v0.4.3 (A5): trace timeout with elapsed-ms.\n this.log?.debug(`HTTP timeout ${method} ${path} (${Date.now() - startedAt}ms)`);\n reject(new Error(\"Request timeout\"));\n });\n\n req.on(\"error\", err => {\n cleanup();\n // v0.4.3 (A7): trace network / abort / TLS / DNS errors with elapsed.\n // Also catches the abort case (req.destroy(Error(\"Request aborted\")))\n // \u2014 A6 deliberately not emitted to avoid double-log.\n this.log?.debug(`HTTP error ${method} ${path} (${Date.now() - startedAt}ms): ${err.message}`);\n reject(err);\n });\n\n if (body) {\n req.write(JSON.stringify(body));\n }\n req.end();\n });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["_a", "err"]
|
|
7
7
|
}
|
|
@@ -149,6 +149,7 @@ class StateManager {
|
|
|
149
149
|
const n = parseInt(raw, 10);
|
|
150
150
|
return Number.isFinite(n) ? n : 0;
|
|
151
151
|
}
|
|
152
|
+
this.adapter.log.debug(`parseStatus drift: ${JSON.stringify(raw)} (type ${typeof raw}) \u2192 0 (delivered fallback)`);
|
|
152
153
|
return 0;
|
|
153
154
|
}
|
|
154
155
|
/**
|
|
@@ -170,6 +171,9 @@ class StateManager {
|
|
|
170
171
|
const rawKey = StateManager.rawIdKey(delivery);
|
|
171
172
|
if (owner !== void 0 && owner !== rawKey) {
|
|
172
173
|
const suffixed = `${id}__${StateManager.shortHash(rawKey)}`;
|
|
174
|
+
this.adapter.log.debug(
|
|
175
|
+
`packageId collision: bare='${id}' owner='${owner}' new='${rawKey}' \u2192 suffixed='${suffixed}'`
|
|
176
|
+
);
|
|
173
177
|
this.idOwner.set(suffixed, rawKey);
|
|
174
178
|
return suffixed;
|
|
175
179
|
}
|
|
@@ -234,7 +238,11 @@ class StateManager {
|
|
|
234
238
|
});
|
|
235
239
|
const statusCode = this.parseStatus(delivery);
|
|
236
240
|
const labels = import_types.STATUS_LABELS[this.language];
|
|
237
|
-
|
|
241
|
+
let statusText = labels[statusCode];
|
|
242
|
+
if (!statusText) {
|
|
243
|
+
this.adapter.log.debug(`status code ${statusCode} not in STATUS_LABELS[${this.language}], using fallback`);
|
|
244
|
+
statusText = `Unknown (${statusCode})`;
|
|
245
|
+
}
|
|
238
246
|
await Promise.all([
|
|
239
247
|
this.createAndSet(`${devicePath}.carrier`, asName((0, import_i18n_states.tName)("carrier")), "string", "text", carrierName),
|
|
240
248
|
this.createAndSet(`${devicePath}.status`, asName((0, import_i18n_states.tName)("status")), "string", "text", statusText),
|
|
@@ -293,6 +301,9 @@ class StateManager {
|
|
|
293
301
|
*/
|
|
294
302
|
async updateSummary(activeDeliveries) {
|
|
295
303
|
const todayDeliveries = activeDeliveries.filter((d) => this.isToday(d, this.parseStatus(d)));
|
|
304
|
+
this.adapter.log.debug(
|
|
305
|
+
`updateSummary: ${activeDeliveries.length} active, ${todayDeliveries.length} expected today`
|
|
306
|
+
);
|
|
296
307
|
await Promise.all([
|
|
297
308
|
this.createAndSet(
|
|
298
309
|
"summary.activeCount",
|
|
@@ -323,6 +334,7 @@ class StateManager {
|
|
|
323
334
|
endkey: `${this.adapter.namespace}.deliveries.\u9999`
|
|
324
335
|
});
|
|
325
336
|
if (!(objects == null ? void 0 : objects.rows)) {
|
|
337
|
+
this.adapter.log.debug("cleanupDeliveries: no objects view available, skipping");
|
|
326
338
|
return;
|
|
327
339
|
}
|
|
328
340
|
const toDelete = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/state-manager.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AdapterInstance } from \"@iobroker/adapter-core\";\nimport { coerceFiniteNumber } from \"./coerce\";\nimport type { StateName } from \"./i18n-states\";\nimport { tName } from \"./i18n-states\";\nimport type { ParcelDelivery } from \"./types\";\nimport { STATUS_LABELS, SUPPORTED_LANGUAGES, FALLBACK_LANGUAGE } from \"./types\";\n\n/** Status codes that have expected delivery date/time */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Translation-object cast helper \u2014 ioBroker's `common.name` accepts `StringOrTranslated`.\n *\n * @param name Translation object from {@link STATE_NAMES}.\n */\nfunction asName(name: StateName): ioBroker.StringOrTranslated {\n return name;\n}\n\n/** Delivery-estimate labels keyed by language code. Keys must match STATUS_LABELS. */\nconst ESTIMATE_LABELS: Record<string, Record<string, string>> = {\n de: {\n overdue: \"\u00FCberf\u00E4llig\",\n today: \"heute\",\n tomorrow: \"morgen\",\n days: \"in %d Tagen\",\n },\n en: {\n overdue: \"overdue\",\n today: \"today\",\n tomorrow: \"tomorrow\",\n days: \"in %d days\",\n },\n ru: {\n overdue: \"\u043F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u043E\",\n today: \"\u0441\u0435\u0433\u043E\u0434\u043D\u044F\",\n tomorrow: \"\u0437\u0430\u0432\u0442\u0440\u0430\",\n days: \"\u0447\u0435\u0440\u0435\u0437 %d \u0434\u043D.\",\n },\n pt: {\n overdue: \"atrasado\",\n today: \"hoje\",\n tomorrow: \"amanh\u00E3\",\n days: \"em %d dias\",\n },\n nl: {\n overdue: \"te laat\",\n today: \"vandaag\",\n tomorrow: \"morgen\",\n days: \"over %d dagen\",\n },\n fr: {\n overdue: \"en retard\",\n today: \"aujourd'hui\",\n tomorrow: \"demain\",\n days: \"dans %d jours\",\n },\n it: {\n overdue: \"in ritardo\",\n today: \"oggi\",\n tomorrow: \"domani\",\n days: \"tra %d giorni\",\n },\n es: {\n overdue: \"atrasado\",\n today: \"hoy\",\n tomorrow: \"ma\u00F1ana\",\n days: \"en %d d\u00EDas\",\n },\n pl: {\n overdue: \"zaleg\u0142e\",\n today: \"dzisiaj\",\n tomorrow: \"jutro\",\n days: \"za %d dni\",\n },\n uk: {\n overdue: \"\u043F\u0440\u043E\u0441\u0442\u0440\u043E\u0447\u0435\u043D\u043E\",\n today: \"\u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456\",\n tomorrow: \"\u0437\u0430\u0432\u0442\u0440\u0430\",\n days: \"\u0447\u0435\u0440\u0435\u0437 %d \u0434\u043D.\",\n },\n \"zh-cn\": {\n overdue: \"\u5DF2\u903E\u671F\",\n today: \"\u4ECA\u5929\",\n tomorrow: \"\u660E\u5929\",\n days: \"%d \u5929\u540E\",\n },\n};\n\n/**\n * Resolve a language code to one that has labels. Falls back to English\n * when the system language is not one of the supported ioBroker languages.\n *\n * @param language Raw language code (e.g. from system.config.language)\n */\nexport function resolveLanguage(language: unknown): string {\n if (typeof language === \"string\" && SUPPORTED_LANGUAGES.includes(language)) {\n return language;\n }\n return FALLBACK_LANGUAGE;\n}\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\n private language: string;\n /**\n * Cache of state IDs that have already passed `setObjectNotExistsAsync`.\n * Skips repeat DB lookups on the hot path \u2014 each poll touches ~11 states\n * per delivery, and most deliveries see no schema change between polls.\n * On `cleanupDeliveries`, IDs of removed packages are dropped so a re-add\n * triggers a fresh creation.\n */\n private readonly createdIds = new Set<string>();\n\n /**\n * @param adapter The ioBroker adapter instance\n * @param language Language code from system.config.language (falls back to English)\n */\n constructor(adapter: AdapterInstance, language: string) {\n this.adapter = adapter;\n this.language = resolveLanguage(language);\n }\n\n /**\n * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).\n * API-drift guard: returns \"unknown\" for non-string input.\n *\n * @param name Raw value to sanitize (any type)\n */\n sanitize(name: unknown): string {\n if (typeof name !== \"string\") {\n return \"unknown\";\n }\n return (\n name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 50) || \"unknown\"\n );\n }\n\n /**\n * Parse the status code from a delivery. API documents `status_code` as\n * a numeric string, but we accept numbers too and fall back to 0 for drift.\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n const raw = delivery.status_code as unknown;\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return Math.trunc(raw);\n }\n if (typeof raw === \"string\") {\n const n = parseInt(raw, 10);\n return Number.isFinite(n) ? n : 0;\n }\n return 0;\n }\n\n /**\n * Build a unique package ID from a delivery.\n *\n * v0.4.2 (S3): when the bare `sanitize(tracking_number)` collides with\n * another active package (e.g. two trackings differ only in special\n * chars that strip down to the same id), append a stable hash of the\n * full tracking number so both end up at distinct state IDs.\n *\n * @param delivery The delivery to build an ID for\n */\n packageId(delivery: ParcelDelivery): string {\n let id = this.sanitize(delivery.tracking_number);\n // API-drift guard: only string values extend the id\n if (typeof delivery.extra_information === \"string\" && delivery.extra_information.length > 0) {\n id += `_${this.sanitize(delivery.extra_information)}`;\n }\n // v0.4.2 (S3): collision suffix when two distinct (raw) trackings would\n // collapse to the same id. Bare id is kept as long as it's unique\n // within this poll (back-compat with existing installs).\n const owner = this.idOwner.get(id);\n const rawKey = StateManager.rawIdKey(delivery);\n if (owner !== undefined && owner !== rawKey) {\n const suffixed = `${id}__${StateManager.shortHash(rawKey)}`;\n this.idOwner.set(suffixed, rawKey);\n return suffixed;\n }\n this.idOwner.set(id, rawKey);\n return id;\n }\n\n /**\n * v0.4.2 (S3): build a stable raw-key for collision tracking.\n *\n * @param delivery The delivery whose raw tracking identifies it.\n */\n private static rawIdKey(delivery: ParcelDelivery): string {\n const t = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const e = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n return `${t}\u0000${e}`;\n }\n\n /**\n * v0.4.2 (S3): FNV-1a 32-bit short hash \u2192 6 hex chars.\n *\n * @param s Input string to hash.\n */\n private static shortHash(s: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, \"0\").slice(0, 6);\n }\n\n /**\n * v0.4.2 (S3): which raw-tracking-key currently \"owns\" each sanitized id\n * within the running poll. Cleared via `resetIdOwners()` between polls so\n * the same delivery keeps its bare id as long as it's unique.\n */\n private readonly idOwner = new Map<string, string>();\n\n /**\n * v0.4.2 (S3): reset the per-poll collision tracker. Call from main.ts\n * before iterating deliveries so the bare id always wins for the first\n * occurrence in each poll.\n */\n resetPollState(): void {\n this.idOwner.clear();\n }\n\n /**\n * Update or create all states for a delivery.\n *\n * @param delivery The delivery data from API\n * @param carrierName Resolved carrier display name\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string): Promise<void> {\n const pkgId = this.packageId(delivery);\n const devicePath = `deliveries.${pkgId}`;\n\n const description = typeof delivery.description === \"string\" ? delivery.description : \"\";\n const trackingNumber = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const extraInfo = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n\n await this.adapter.extendObjectAsync(devicePath, {\n type: \"device\",\n common: {\n name: description || `Package ${trackingNumber || pkgId}`,\n },\n native: {},\n });\n\n const statusCode = this.parseStatus(delivery);\n const labels = STATUS_LABELS[this.language];\n const statusText = labels[statusCode] || `Unknown (${statusCode})`;\n\n await Promise.all([\n this.createAndSet(`${devicePath}.carrier`, asName(tName(\"carrier\")), \"string\", \"text\", carrierName),\n this.createAndSet(`${devicePath}.status`, asName(tName(\"status\")), \"string\", \"text\", statusText),\n this.createAndSet(`${devicePath}.statusCode`, asName(tName(\"statusCode\")), \"number\", \"value\", statusCode),\n this.createAndSet(`${devicePath}.description`, asName(tName(\"description\")), \"string\", \"text\", description),\n this.createAndSet(\n `${devicePath}.trackingNumber`,\n asName(tName(\"trackingNumber\")),\n \"string\",\n \"text\",\n trackingNumber,\n ),\n this.createAndSet(`${devicePath}.extraInfo`, asName(tName(\"extraInfo\")), \"string\", \"text\", extraInfo),\n this.createAndSet(\n `${devicePath}.deliveryWindow`,\n asName(tName(\"deliveryWindow\")),\n \"string\",\n \"text\",\n this.calculateDeliveryWindow(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.deliveryEstimate`,\n asName(tName(\"deliveryEstimate\")),\n \"string\",\n \"text\",\n this.calculateDeliveryEstimate(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.lastEvent`,\n asName(tName(\"lastEvent\")),\n \"string\",\n \"text\",\n this.formatLastEvent(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastLocation`,\n asName(tName(\"lastLocation\")),\n \"string\",\n \"text\",\n this.extractLastLocation(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastUpdated`,\n asName(tName(\"lastUpdated\")),\n \"string\",\n \"date\",\n new Date().toISOString(),\n ),\n ]);\n }\n\n /**\n * Update summary states. Expects already-filtered active deliveries.\n * The `summary` channel itself is declared via io-package.json instanceObjects.\n *\n * @param activeDeliveries Only active (non-delivered) deliveries\n */\n async updateSummary(activeDeliveries: ParcelDelivery[]): Promise<void> {\n const todayDeliveries = activeDeliveries.filter(d => this.isToday(d, this.parseStatus(d)));\n\n await Promise.all([\n this.createAndSet(\n \"summary.activeCount\",\n asName(tName(\"activeCount\")),\n \"number\",\n \"value\",\n activeDeliveries.length,\n ),\n this.createAndSet(\"summary.todayCount\", asName(tName(\"todayCount\")), \"number\", \"value\", todayDeliveries.length),\n this.createAndSet(\n \"summary.deliveryWindow\",\n asName(tName(\"summaryDeliveryWindow\")),\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer active.\n *\n * @param activeIds List of currently active package IDs\n */\n async cleanupDeliveries(activeIds: string[]): Promise<void> {\n const activeSet = new Set(activeIds.map(id => `deliveries.${id}`));\n\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.\u9999`,\n });\n if (!objects?.rows) {\n return;\n }\n\n // v0.4.2 (S1): collect first, then delete in parallel. Earlier each\n // stale package took a sequential broker round-trip.\n const toDelete: string[] = [];\n for (const row of objects.rows) {\n const relativeId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n if (relativeId.startsWith(\"deliveries.\") && !activeSet.has(relativeId)) {\n toDelete.push(relativeId);\n }\n }\n\n await Promise.all(\n toDelete.map(async relativeId => {\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n // v0.4.2 (S2): snapshot to array first \u2014 defensive against any future\n // engine that diverges from spec on Set.delete during for-of iteration.\n for (const id of [...this.createdIds]) {\n if (id === relativeId || id.startsWith(`${relativeId}.`)) {\n this.createdIds.delete(id);\n }\n }\n }),\n );\n }\n\n /**\n * Calculate delivery time window \u2014 only from Unix timestamps.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(delivery: ParcelDelivery, statusCode: number): string {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return \"\";\n }\n\n const formatTime = (timestamp: unknown): string | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const d = new Date(ts * 1000);\n if (Number.isNaN(d.getTime())) {\n return null;\n }\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n };\n\n const start = formatTime(delivery.timestamp_expected);\n const end = formatTime(delivery.timestamp_expected_end);\n\n if (!start) {\n return \"\";\n }\n return end ? `${start} - ${end}` : start;\n }\n\n /**\n * Days from today to the expected delivery date. Returns null when the\n * delivery has no usable expected date or is in a non-trackable status.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private computeDiffDays(delivery: ParcelDelivery, statusCode: number): number | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n\n let expectedDate: Date | null = null;\n const ts = coerceFiniteNumber(delivery.timestamp_expected);\n if (ts !== null && ts > 0) {\n expectedDate = new Date(ts * 1000);\n } else if (typeof delivery.date_expected === \"string\" && delivery.date_expected.length > 0) {\n expectedDate = new Date(delivery.date_expected);\n }\n\n if (!expectedDate || isNaN(expectedDate.getTime())) {\n return null;\n }\n\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const expectedStart = new Date(expectedDate.getFullYear(), expectedDate.getMonth(), expectedDate.getDate());\n return Math.round((expectedStart.getTime() - todayStart.getTime()) / (1000 * 60 * 60 * 24));\n }\n\n /**\n * Calculate human-readable delivery estimate.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryEstimate(delivery: ParcelDelivery, statusCode: number): string {\n const diffDays = this.computeDiffDays(delivery, statusCode);\n if (diffDays === null) {\n return \"\";\n }\n const l = ESTIMATE_LABELS[this.language];\n if (diffDays < 0) {\n return l.overdue;\n }\n if (diffDays === 0) {\n return l.today;\n }\n if (diffDays === 1) {\n return l.tomorrow;\n }\n return l.days.replace(\"%d\", String(diffDays));\n }\n\n /**\n * Whether the delivery is expected today. Language-agnostic, used by the\n * summary filter so `todayCount` works across all languages.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private isToday(delivery: ParcelDelivery, statusCode: number): boolean {\n return this.computeDiffDays(delivery, statusCode) === 0;\n }\n\n /**\n * Format the latest tracking event.\n *\n * @param delivery The delivery data\n */\n private formatLastEvent(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n const parts: string[] = [];\n if (typeof latest.event === \"string\" && latest.event.length > 0) {\n parts.push(latest.event);\n }\n if (typeof latest.date === \"string\" && latest.date.length > 0) {\n parts.push(latest.date);\n }\n return parts.join(\" - \");\n }\n\n /**\n * Extract location from latest event.\n *\n * @param delivery The delivery data\n */\n private extractLastLocation(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Calculate combined delivery window for today's packages.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const windows = todayDeliveries\n .map(d => this.calculateDeliveryWindow(d, this.parseStatus(d)))\n .filter(w => w.length > 0);\n\n if (windows.length === 0) {\n return \"\";\n }\n if (windows.length === 1) {\n return windows[0];\n }\n\n const times: {\n /** Window start */ start: string;\n /** Window end */ end: string;\n }[] = [];\n for (const w of windows) {\n const match = w.match(/(\\d{2}:\\d{2})(?:\\s*-\\s*(\\d{2}:\\d{2}))?/);\n if (match) {\n times.push({ start: match[1], end: match[2] || match[1] });\n }\n }\n\n if (times.length === 0) {\n return \"\";\n }\n\n times.sort((a, b) => a.start.localeCompare(b.start));\n return `${times[0].start} - ${times[times.length - 1].end}`;\n }\n\n /**\n * Create/extend a read-only state and set its value. Skips the\n * `setObjectNotExistsAsync` round-trip once the ID is in the cache \u2014\n * states are static after first creation; only the value changes per poll.\n *\n * @param id State ID relative to adapter namespace\n * @param name Display name (translation object or plain string)\n * @param type Value type\n * @param role ioBroker role\n * @param val Value to set\n */\n private async createAndSet(\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ): Promise<void> {\n if (!this.createdIds.has(id)) {\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: { name, type, role, read: true, write: false },\n native: {},\n });\n this.createdIds.add(id);\n }\n await this.adapter.setStateAsync(id, { val, ack: true });\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,oBAAmC;AAEnC,yBAAsB;AAEtB,mBAAsE;AAGtE,MAAM,qBAAqB,oBAAI,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;AAO5C,SAAS,OAAO,MAA8C;AAC5D,SAAO;AACT;AAGA,MAAM,kBAA0D;AAAA,EAC9D,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,IAAI;AAAA,IACF,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AAAA,EACA,SAAS;AAAA,IACP,SAAS;AAAA,IACT,OAAO;AAAA,IACP,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AACF;AAQO,SAAS,gBAAgB,UAA2B;AACzD,MAAI,OAAO,aAAa,YAAY,iCAAoB,SAAS,QAAQ,GAAG;AAC1E,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGO,MAAM,aAAa;AAAA,EAChB;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQS,aAAa,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA,EAM9C,YAAY,SAA0B,UAAkB;AACtD,SAAK,UAAU;AACf,SAAK,WAAW,gBAAgB,QAAQ;AAAA,EAC1C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,SAAS,MAAuB;AAC9B,QAAI,OAAO,SAAS,UAAU;AAC5B,aAAO;AAAA,IACT;AACA,WACE,KACG,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE,KAAK;AAAA,EAEvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,YAAY,UAAkC;AAC5C,UAAM,MAAM,SAAS;AACrB,QAAI,OAAO,QAAQ,YAAY,OAAO,SAAS,GAAG,GAAG;AACnD,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB;AACA,QAAI,OAAO,QAAQ,UAAU;AAC3B,YAAM,IAAI,SAAS,KAAK,EAAE;AAC1B,aAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,IAClC;
|
|
4
|
+
"sourcesContent": ["import type { AdapterInstance } from \"@iobroker/adapter-core\";\nimport { coerceFiniteNumber } from \"./coerce\";\nimport type { StateName } from \"./i18n-states\";\nimport { tName } from \"./i18n-states\";\nimport type { ParcelDelivery } from \"./types\";\nimport { STATUS_LABELS, SUPPORTED_LANGUAGES, FALLBACK_LANGUAGE } from \"./types\";\n\n/** Status codes that have expected delivery date/time */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Translation-object cast helper \u2014 ioBroker's `common.name` accepts `StringOrTranslated`.\n *\n * @param name Translation object from {@link STATE_NAMES}.\n */\nfunction asName(name: StateName): ioBroker.StringOrTranslated {\n return name;\n}\n\n/** Delivery-estimate labels keyed by language code. Keys must match STATUS_LABELS. */\nconst ESTIMATE_LABELS: Record<string, Record<string, string>> = {\n de: {\n overdue: \"\u00FCberf\u00E4llig\",\n today: \"heute\",\n tomorrow: \"morgen\",\n days: \"in %d Tagen\",\n },\n en: {\n overdue: \"overdue\",\n today: \"today\",\n tomorrow: \"tomorrow\",\n days: \"in %d days\",\n },\n ru: {\n overdue: \"\u043F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u043E\",\n today: \"\u0441\u0435\u0433\u043E\u0434\u043D\u044F\",\n tomorrow: \"\u0437\u0430\u0432\u0442\u0440\u0430\",\n days: \"\u0447\u0435\u0440\u0435\u0437 %d \u0434\u043D.\",\n },\n pt: {\n overdue: \"atrasado\",\n today: \"hoje\",\n tomorrow: \"amanh\u00E3\",\n days: \"em %d dias\",\n },\n nl: {\n overdue: \"te laat\",\n today: \"vandaag\",\n tomorrow: \"morgen\",\n days: \"over %d dagen\",\n },\n fr: {\n overdue: \"en retard\",\n today: \"aujourd'hui\",\n tomorrow: \"demain\",\n days: \"dans %d jours\",\n },\n it: {\n overdue: \"in ritardo\",\n today: \"oggi\",\n tomorrow: \"domani\",\n days: \"tra %d giorni\",\n },\n es: {\n overdue: \"atrasado\",\n today: \"hoy\",\n tomorrow: \"ma\u00F1ana\",\n days: \"en %d d\u00EDas\",\n },\n pl: {\n overdue: \"zaleg\u0142e\",\n today: \"dzisiaj\",\n tomorrow: \"jutro\",\n days: \"za %d dni\",\n },\n uk: {\n overdue: \"\u043F\u0440\u043E\u0441\u0442\u0440\u043E\u0447\u0435\u043D\u043E\",\n today: \"\u0441\u044C\u043E\u0433\u043E\u0434\u043D\u0456\",\n tomorrow: \"\u0437\u0430\u0432\u0442\u0440\u0430\",\n days: \"\u0447\u0435\u0440\u0435\u0437 %d \u0434\u043D.\",\n },\n \"zh-cn\": {\n overdue: \"\u5DF2\u903E\u671F\",\n today: \"\u4ECA\u5929\",\n tomorrow: \"\u660E\u5929\",\n days: \"%d \u5929\u540E\",\n },\n};\n\n/**\n * Resolve a language code to one that has labels. Falls back to English\n * when the system language is not one of the supported ioBroker languages.\n *\n * @param language Raw language code (e.g. from system.config.language)\n */\nexport function resolveLanguage(language: unknown): string {\n if (typeof language === \"string\" && SUPPORTED_LANGUAGES.includes(language)) {\n return language;\n }\n return FALLBACK_LANGUAGE;\n}\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\n private language: string;\n /**\n * Cache of state IDs that have already passed `setObjectNotExistsAsync`.\n * Skips repeat DB lookups on the hot path \u2014 each poll touches ~11 states\n * per delivery, and most deliveries see no schema change between polls.\n * On `cleanupDeliveries`, IDs of removed packages are dropped so a re-add\n * triggers a fresh creation.\n */\n private readonly createdIds = new Set<string>();\n\n /**\n * @param adapter The ioBroker adapter instance\n * @param language Language code from system.config.language (falls back to English)\n */\n constructor(adapter: AdapterInstance, language: string) {\n this.adapter = adapter;\n this.language = resolveLanguage(language);\n }\n\n /**\n * Sanitize a string for use as ioBroker object ID (see adapter.FORBIDDEN_CHARS).\n * API-drift guard: returns \"unknown\" for non-string input.\n *\n * @param name Raw value to sanitize (any type)\n */\n sanitize(name: unknown): string {\n if (typeof name !== \"string\") {\n return \"unknown\";\n }\n return (\n name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 50) || \"unknown\"\n );\n }\n\n /**\n * Parse the status code from a delivery. API documents `status_code` as\n * a numeric string, but we accept numbers too and fall back to 0 for drift.\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n const raw = delivery.status_code as unknown;\n if (typeof raw === \"number\" && Number.isFinite(raw)) {\n return Math.trunc(raw);\n }\n if (typeof raw === \"string\") {\n const n = parseInt(raw, 10);\n return Number.isFinite(n) ? n : 0;\n }\n // v0.4.3 (C2): trace status_code drift (non-string/non-number). Without\n // this, the fallback to 0 (\"delivered\") silently makes packages disappear\n // from active states with no log entry explaining why.\n this.adapter.log.debug(`parseStatus drift: ${JSON.stringify(raw)} (type ${typeof raw}) \u2192 0 (delivered fallback)`);\n return 0;\n }\n\n /**\n * Build a unique package ID from a delivery.\n *\n * v0.4.2 (S3): when the bare `sanitize(tracking_number)` collides with\n * another active package (e.g. two trackings differ only in special\n * chars that strip down to the same id), append a stable hash of the\n * full tracking number so both end up at distinct state IDs.\n *\n * @param delivery The delivery to build an ID for\n */\n packageId(delivery: ParcelDelivery): string {\n let id = this.sanitize(delivery.tracking_number);\n // API-drift guard: only string values extend the id\n if (typeof delivery.extra_information === \"string\" && delivery.extra_information.length > 0) {\n id += `_${this.sanitize(delivery.extra_information)}`;\n }\n // v0.4.2 (S3): collision suffix when two distinct (raw) trackings would\n // collapse to the same id. Bare id is kept as long as it's unique\n // within this poll (back-compat with existing installs).\n const owner = this.idOwner.get(id);\n const rawKey = StateManager.rawIdKey(delivery);\n if (owner !== undefined && owner !== rawKey) {\n const suffixed = `${id}__${StateManager.shortHash(rawKey)}`;\n // v0.4.3 (C3): trace the collision-suffix path. Rare event but the\n // resulting state-id divergence is hard to diagnose without a log.\n this.adapter.log.debug(\n `packageId collision: bare='${id}' owner='${owner}' new='${rawKey}' \u2192 suffixed='${suffixed}'`,\n );\n this.idOwner.set(suffixed, rawKey);\n return suffixed;\n }\n this.idOwner.set(id, rawKey);\n return id;\n }\n\n /**\n * v0.4.2 (S3): build a stable raw-key for collision tracking.\n *\n * @param delivery The delivery whose raw tracking identifies it.\n */\n private static rawIdKey(delivery: ParcelDelivery): string {\n const t = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const e = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n return `${t}\u0000${e}`;\n }\n\n /**\n * v0.4.2 (S3): FNV-1a 32-bit short hash \u2192 6 hex chars.\n *\n * @param s Input string to hash.\n */\n private static shortHash(s: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, \"0\").slice(0, 6);\n }\n\n /**\n * v0.4.2 (S3): which raw-tracking-key currently \"owns\" each sanitized id\n * within the running poll. Cleared via `resetIdOwners()` between polls so\n * the same delivery keeps its bare id as long as it's unique.\n */\n private readonly idOwner = new Map<string, string>();\n\n /**\n * v0.4.2 (S3): reset the per-poll collision tracker. Call from main.ts\n * before iterating deliveries so the bare id always wins for the first\n * occurrence in each poll.\n */\n resetPollState(): void {\n this.idOwner.clear();\n }\n\n /**\n * Update or create all states for a delivery.\n *\n * @param delivery The delivery data from API\n * @param carrierName Resolved carrier display name\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string): Promise<void> {\n const pkgId = this.packageId(delivery);\n const devicePath = `deliveries.${pkgId}`;\n\n const description = typeof delivery.description === \"string\" ? delivery.description : \"\";\n const trackingNumber = typeof delivery.tracking_number === \"string\" ? delivery.tracking_number : \"\";\n const extraInfo = typeof delivery.extra_information === \"string\" ? delivery.extra_information : \"\";\n\n await this.adapter.extendObjectAsync(devicePath, {\n type: \"device\",\n common: {\n name: description || `Package ${trackingNumber || pkgId}`,\n },\n native: {},\n });\n\n const statusCode = this.parseStatus(delivery);\n const labels = STATUS_LABELS[this.language];\n let statusText = labels[statusCode];\n if (!statusText) {\n // v0.4.3 (E3): trace unknown status-code (API drift). A future\n // parcel.app status (e.g. 9, 10) would render as \"Unknown (N)\"\n // without any log clue that the codes table is out of date.\n this.adapter.log.debug(`status code ${statusCode} not in STATUS_LABELS[${this.language}], using fallback`);\n statusText = `Unknown (${statusCode})`;\n }\n\n await Promise.all([\n this.createAndSet(`${devicePath}.carrier`, asName(tName(\"carrier\")), \"string\", \"text\", carrierName),\n this.createAndSet(`${devicePath}.status`, asName(tName(\"status\")), \"string\", \"text\", statusText),\n this.createAndSet(`${devicePath}.statusCode`, asName(tName(\"statusCode\")), \"number\", \"value\", statusCode),\n this.createAndSet(`${devicePath}.description`, asName(tName(\"description\")), \"string\", \"text\", description),\n this.createAndSet(\n `${devicePath}.trackingNumber`,\n asName(tName(\"trackingNumber\")),\n \"string\",\n \"text\",\n trackingNumber,\n ),\n this.createAndSet(`${devicePath}.extraInfo`, asName(tName(\"extraInfo\")), \"string\", \"text\", extraInfo),\n this.createAndSet(\n `${devicePath}.deliveryWindow`,\n asName(tName(\"deliveryWindow\")),\n \"string\",\n \"text\",\n this.calculateDeliveryWindow(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.deliveryEstimate`,\n asName(tName(\"deliveryEstimate\")),\n \"string\",\n \"text\",\n this.calculateDeliveryEstimate(delivery, statusCode),\n ),\n this.createAndSet(\n `${devicePath}.lastEvent`,\n asName(tName(\"lastEvent\")),\n \"string\",\n \"text\",\n this.formatLastEvent(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastLocation`,\n asName(tName(\"lastLocation\")),\n \"string\",\n \"text\",\n this.extractLastLocation(delivery),\n ),\n this.createAndSet(\n `${devicePath}.lastUpdated`,\n asName(tName(\"lastUpdated\")),\n \"string\",\n \"date\",\n new Date().toISOString(),\n ),\n ]);\n }\n\n /**\n * Update summary states. Expects already-filtered active deliveries.\n * The `summary` channel itself is declared via io-package.json instanceObjects.\n *\n * @param activeDeliveries Only active (non-delivered) deliveries\n */\n async updateSummary(activeDeliveries: ParcelDelivery[]): Promise<void> {\n const todayDeliveries = activeDeliveries.filter(d => this.isToday(d, this.parseStatus(d)));\n // v0.4.3 (E1): trace summary refresh \u2014 ~144/day at the default poll\n // interval, kept short (counts only).\n this.adapter.log.debug(\n `updateSummary: ${activeDeliveries.length} active, ${todayDeliveries.length} expected today`,\n );\n\n await Promise.all([\n this.createAndSet(\n \"summary.activeCount\",\n asName(tName(\"activeCount\")),\n \"number\",\n \"value\",\n activeDeliveries.length,\n ),\n this.createAndSet(\"summary.todayCount\", asName(tName(\"todayCount\")), \"number\", \"value\", todayDeliveries.length),\n this.createAndSet(\n \"summary.deliveryWindow\",\n asName(tName(\"summaryDeliveryWindow\")),\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer active.\n *\n * @param activeIds List of currently active package IDs\n */\n async cleanupDeliveries(activeIds: string[]): Promise<void> {\n const activeSet = new Set(activeIds.map(id => `deliveries.${id}`));\n\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.\u9999`,\n });\n if (!objects?.rows) {\n // v0.4.3 (E2): trace the no-op path \u2014 happens on fresh installs or\n // when getObjectViewAsync returns falsy. Without this the early-return\n // is invisible.\n this.adapter.log.debug(\"cleanupDeliveries: no objects view available, skipping\");\n return;\n }\n\n // v0.4.2 (S1): collect first, then delete in parallel. Earlier each\n // stale package took a sequential broker round-trip.\n const toDelete: string[] = [];\n for (const row of objects.rows) {\n const relativeId = row.id.replace(`${this.adapter.namespace}.`, \"\");\n if (relativeId.startsWith(\"deliveries.\") && !activeSet.has(relativeId)) {\n toDelete.push(relativeId);\n }\n }\n\n await Promise.all(\n toDelete.map(async relativeId => {\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n // v0.4.2 (S2): snapshot to array first \u2014 defensive against any future\n // engine that diverges from spec on Set.delete during for-of iteration.\n for (const id of [...this.createdIds]) {\n if (id === relativeId || id.startsWith(`${relativeId}.`)) {\n this.createdIds.delete(id);\n }\n }\n }),\n );\n }\n\n /**\n * Calculate delivery time window \u2014 only from Unix timestamps.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(delivery: ParcelDelivery, statusCode: number): string {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return \"\";\n }\n\n const formatTime = (timestamp: unknown): string | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const d = new Date(ts * 1000);\n if (Number.isNaN(d.getTime())) {\n return null;\n }\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n };\n\n const start = formatTime(delivery.timestamp_expected);\n const end = formatTime(delivery.timestamp_expected_end);\n\n if (!start) {\n return \"\";\n }\n return end ? `${start} - ${end}` : start;\n }\n\n /**\n * Days from today to the expected delivery date. Returns null when the\n * delivery has no usable expected date or is in a non-trackable status.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private computeDiffDays(delivery: ParcelDelivery, statusCode: number): number | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n\n let expectedDate: Date | null = null;\n const ts = coerceFiniteNumber(delivery.timestamp_expected);\n if (ts !== null && ts > 0) {\n expectedDate = new Date(ts * 1000);\n } else if (typeof delivery.date_expected === \"string\" && delivery.date_expected.length > 0) {\n expectedDate = new Date(delivery.date_expected);\n }\n\n if (!expectedDate || isNaN(expectedDate.getTime())) {\n return null;\n }\n\n const now = new Date();\n const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n const expectedStart = new Date(expectedDate.getFullYear(), expectedDate.getMonth(), expectedDate.getDate());\n return Math.round((expectedStart.getTime() - todayStart.getTime()) / (1000 * 60 * 60 * 24));\n }\n\n /**\n * Calculate human-readable delivery estimate.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryEstimate(delivery: ParcelDelivery, statusCode: number): string {\n const diffDays = this.computeDiffDays(delivery, statusCode);\n if (diffDays === null) {\n return \"\";\n }\n const l = ESTIMATE_LABELS[this.language];\n if (diffDays < 0) {\n return l.overdue;\n }\n if (diffDays === 0) {\n return l.today;\n }\n if (diffDays === 1) {\n return l.tomorrow;\n }\n return l.days.replace(\"%d\", String(diffDays));\n }\n\n /**\n * Whether the delivery is expected today. Language-agnostic, used by the\n * summary filter so `todayCount` works across all languages.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private isToday(delivery: ParcelDelivery, statusCode: number): boolean {\n return this.computeDiffDays(delivery, statusCode) === 0;\n }\n\n /**\n * Format the latest tracking event.\n *\n * @param delivery The delivery data\n */\n private formatLastEvent(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n const parts: string[] = [];\n if (typeof latest.event === \"string\" && latest.event.length > 0) {\n parts.push(latest.event);\n }\n if (typeof latest.date === \"string\" && latest.date.length > 0) {\n parts.push(latest.date);\n }\n return parts.join(\" - \");\n }\n\n /**\n * Extract location from latest event.\n *\n * @param delivery The delivery data\n */\n private extractLastLocation(delivery: ParcelDelivery): string {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return \"\";\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Calculate combined delivery window for today's packages.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const windows = todayDeliveries\n .map(d => this.calculateDeliveryWindow(d, this.parseStatus(d)))\n .filter(w => w.length > 0);\n\n if (windows.length === 0) {\n return \"\";\n }\n if (windows.length === 1) {\n return windows[0];\n }\n\n const times: {\n /** Window start */ start: string;\n /** Window end */ end: string;\n }[] = [];\n for (const w of windows) {\n const match = w.match(/(\\d{2}:\\d{2})(?:\\s*-\\s*(\\d{2}:\\d{2}))?/);\n if (match) {\n times.push({ start: match[1], end: match[2] || match[1] });\n }\n }\n\n if (times.length === 0) {\n return \"\";\n }\n\n times.sort((a, b) => a.start.localeCompare(b.start));\n return `${times[0].start} - ${times[times.length - 1].end}`;\n }\n\n /**\n * Create/extend a read-only state and set its value. Skips the\n * `setObjectNotExistsAsync` round-trip once the ID is in the cache \u2014\n * states are static after first creation; only the value changes per poll.\n *\n * @param id State ID relative to adapter namespace\n * @param name Display name (translation object or plain string)\n * @param type Value type\n * @param role ioBroker role\n * @param val Value to set\n */\n private async createAndSet(\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ): Promise<void> {\n if (!this.createdIds.has(id)) {\n await this.adapter.setObjectNotExistsAsync(id, {\n type: \"state\",\n common: { name, type, role, read: true, write: false },\n native: {},\n });\n this.createdIds.add(id);\n }\n await this.adapter.setStateAsync(id, { val, ack: true });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -39,6 +39,14 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
39
39
|
rateLimitedUntil = 0;
|
|
40
40
|
lastErrorCode = "";
|
|
41
41
|
failedDeliveries = /* @__PURE__ */ new Set();
|
|
42
|
+
/**
|
|
43
|
+
* v0.4.4: short-lived test-clients spawned from `checkConnection` admin
|
|
44
|
+
* messages. The prod-`this.client` is what `onUnload` cancels, so these
|
|
45
|
+
* need their own registry to be reachable at shutdown. Without this, an
|
|
46
|
+
* admin clicking "Test Connection" right before adapter-stop could keep
|
|
47
|
+
* the process alive past js-controller's 4-second kill deadline.
|
|
48
|
+
*/
|
|
49
|
+
testClients = /* @__PURE__ */ new Set();
|
|
42
50
|
unhandledRejectionHandler = null;
|
|
43
51
|
uncaughtExceptionHandler = null;
|
|
44
52
|
/** ioBroker system language — read once in `onReady` from `system.config`. EN fallback. */
|
|
@@ -71,22 +79,27 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
71
79
|
}
|
|
72
80
|
async onReady() {
|
|
73
81
|
var _a, _b;
|
|
82
|
+
this.log.debug(
|
|
83
|
+
`onReady: starting (pollInterval=${JSON.stringify(this.config.pollInterval)}, autoRemoveDelivered=${this.config.autoRemoveDelivered})`
|
|
84
|
+
);
|
|
74
85
|
const sysConfig = await this.getForeignObjectAsync("system.config");
|
|
75
86
|
const language = (_b = (_a = sysConfig == null ? void 0 : sysConfig.common) == null ? void 0 : _a.language) != null ? _b : "";
|
|
76
87
|
if (typeof language === "string" && language.length > 0) {
|
|
77
88
|
this.systemLang = language;
|
|
78
89
|
}
|
|
90
|
+
this.log.debug(`system language: '${language}' \u2192 using '${this.systemLang}'`);
|
|
79
91
|
await this.setStateAsync("info.connection", { val: false, ack: true });
|
|
80
92
|
const { apiKey } = this.config;
|
|
81
93
|
if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {
|
|
82
94
|
this.log.error("No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings");
|
|
83
95
|
return;
|
|
84
96
|
}
|
|
85
|
-
this.client = new import_parcel_client.ParcelClient(apiKey.trim());
|
|
97
|
+
this.client = new import_parcel_client.ParcelClient(apiKey.trim(), { debug: (m) => this.log.debug(m) });
|
|
86
98
|
this.stateManager = new import_state_manager.StateManager(this, language);
|
|
87
99
|
await this.cleanupObsoleteStates();
|
|
88
100
|
await this.poll();
|
|
89
101
|
const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);
|
|
102
|
+
this.log.debug(`pollInterval: raw=${JSON.stringify(this.config.pollInterval)} resolved=${interval}min`);
|
|
90
103
|
const intervalMs = interval * 60 * 1e3;
|
|
91
104
|
this.pollTimer = this.setInterval(() => void this.poll(), intervalMs);
|
|
92
105
|
this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);
|
|
@@ -107,6 +120,10 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
107
120
|
this.pollTimer = void 0;
|
|
108
121
|
}
|
|
109
122
|
(_a = this.client) == null ? void 0 : _a.cancelAll();
|
|
123
|
+
for (const tc of this.testClients) {
|
|
124
|
+
tc.cancelAll();
|
|
125
|
+
}
|
|
126
|
+
this.testClients.clear();
|
|
110
127
|
if (this.unhandledRejectionHandler) {
|
|
111
128
|
process.off("unhandledRejection", this.unhandledRejectionHandler);
|
|
112
129
|
this.unhandledRejectionHandler = null;
|
|
@@ -117,12 +134,14 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
117
134
|
}
|
|
118
135
|
void this.setState("info.connection", { val: false, ack: true }).catch(() => {
|
|
119
136
|
});
|
|
120
|
-
} catch {
|
|
137
|
+
} catch (err) {
|
|
138
|
+
this.log.debug(`onUnload error (ignored): ${(0, import_coerce.errText)(err)}`);
|
|
121
139
|
}
|
|
122
140
|
callback();
|
|
123
141
|
}
|
|
124
142
|
async onMessage(obj) {
|
|
125
143
|
var _a;
|
|
144
|
+
this.log.debug(`onMessage: command='${obj == null ? void 0 : obj.command}' from='${obj == null ? void 0 : obj.from}' has-callback=${!!(obj == null ? void 0 : obj.callback)}`);
|
|
126
145
|
if (!(obj == null ? void 0 : obj.command) || !obj.callback) {
|
|
127
146
|
return;
|
|
128
147
|
}
|
|
@@ -132,16 +151,24 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
132
151
|
const msg = obj.message;
|
|
133
152
|
const key = ((_a = msg == null ? void 0 : msg.apiKey) == null ? void 0 : _a.trim()) || "";
|
|
134
153
|
if (!key || key.length < MIN_API_KEY_LENGTH) {
|
|
154
|
+
this.log.debug("checkConnection: apiKey too short");
|
|
135
155
|
this.sendTo(obj.from, obj.command, { success: false, message: "API key is too short" }, obj.callback);
|
|
136
156
|
return;
|
|
137
157
|
}
|
|
138
|
-
const testClient = new import_parcel_client.ParcelClient(key);
|
|
139
|
-
|
|
140
|
-
|
|
158
|
+
const testClient = new import_parcel_client.ParcelClient(key, { debug: (m) => this.log.debug(m) });
|
|
159
|
+
this.testClients.add(testClient);
|
|
160
|
+
try {
|
|
161
|
+
const result = await testClient.testConnection();
|
|
162
|
+
this.log.debug(`checkConnection: result=${result.success ? "ok" : "fail"} (${result.message})`);
|
|
163
|
+
this.sendTo(obj.from, obj.command, result, obj.callback);
|
|
164
|
+
} finally {
|
|
165
|
+
this.testClients.delete(testClient);
|
|
166
|
+
}
|
|
141
167
|
break;
|
|
142
168
|
}
|
|
143
169
|
case "addDelivery": {
|
|
144
170
|
if (!this.client) {
|
|
171
|
+
this.log.debug("addDelivery: adapter not initialized");
|
|
145
172
|
this.sendTo(
|
|
146
173
|
obj.from,
|
|
147
174
|
obj.command,
|
|
@@ -152,6 +179,7 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
152
179
|
}
|
|
153
180
|
const request = obj.message;
|
|
154
181
|
const addResult = await this.client.addDelivery(request);
|
|
182
|
+
this.log.debug(`addDelivery: '${request == null ? void 0 : request.tracking_number}' result=${addResult.success ? "ok" : "fail"}`);
|
|
155
183
|
this.sendTo(obj.from, obj.command, addResult, obj.callback);
|
|
156
184
|
if (addResult.success) {
|
|
157
185
|
void this.poll();
|
|
@@ -159,9 +187,11 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
159
187
|
break;
|
|
160
188
|
}
|
|
161
189
|
default:
|
|
190
|
+
this.log.debug(`onMessage: unknown command '${obj.command}'`);
|
|
162
191
|
this.sendTo(obj.from, obj.command, { error: "Unknown command" }, obj.callback);
|
|
163
192
|
}
|
|
164
193
|
} catch (err) {
|
|
194
|
+
this.log.debug(`onMessage: '${obj.command}' failed: ${(0, import_coerce.errText)(err)}`);
|
|
165
195
|
this.sendTo(obj.from, obj.command, { success: false, error_message: (0, import_coerce.errText)(err) }, obj.callback);
|
|
166
196
|
}
|
|
167
197
|
}
|
|
@@ -207,6 +237,8 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
207
237
|
return;
|
|
208
238
|
}
|
|
209
239
|
const now = Date.now();
|
|
240
|
+
const autoRemoveMode = this.config.autoRemoveDelivered !== false;
|
|
241
|
+
this.log.debug(`poll: starting (autoRemove=${autoRemoveMode}, lastErrorCode='${this.lastErrorCode}')`);
|
|
210
242
|
if (now < this.rateLimitedUntil) {
|
|
211
243
|
const waitMin = Math.ceil((this.rateLimitedUntil - now) / 6e4);
|
|
212
244
|
this.log.debug(`Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`);
|
|
@@ -219,8 +251,7 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
219
251
|
this.isPolling = true;
|
|
220
252
|
this.lastPollTime = now;
|
|
221
253
|
try {
|
|
222
|
-
const
|
|
223
|
-
const deliveries = await this.client.getDeliveries(autoRemove ? "active" : "recent");
|
|
254
|
+
const deliveries = await this.client.getDeliveries(autoRemoveMode ? "active" : "recent");
|
|
224
255
|
this.rateLimitedUntil = 0;
|
|
225
256
|
if (this.lastErrorCode) {
|
|
226
257
|
this.log.info("Connection restored");
|
|
@@ -228,11 +259,14 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
228
259
|
}
|
|
229
260
|
await this.setStateAsync("info.connection", { val: true, ack: true });
|
|
230
261
|
const activeDeliveries = deliveries.filter((d) => this.stateManager.parseStatus(d) !== 0);
|
|
231
|
-
const visibleDeliveries =
|
|
262
|
+
const visibleDeliveries = autoRemoveMode ? activeDeliveries : deliveries;
|
|
232
263
|
this.stateManager.resetPollState();
|
|
233
264
|
const idResults = await Promise.all(
|
|
234
265
|
visibleDeliveries.map(async (delivery) => {
|
|
235
266
|
try {
|
|
267
|
+
this.log.debug(
|
|
268
|
+
`updateDelivery: '${delivery.tracking_number}' carrier=${delivery.carrier_code} status=${delivery.status_code}`
|
|
269
|
+
);
|
|
236
270
|
const carrierName = await this.client.getCarrierName(delivery.carrier_code);
|
|
237
271
|
await this.stateManager.updateDelivery(delivery, carrierName);
|
|
238
272
|
this.failedDeliveries.delete(delivery.tracking_number);
|
package/build/main.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/main.ts"],
|
|
4
|
-
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { coerceClampedInt, errText } from \"./lib/coerce\";\nimport { ParcelClient } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\nconst MIN_POLL_GAP_MS = 60_000; // Minimum 60s between polls\n/** v0.4.2 (M6): minimum length for an apiKey value to even be considered valid. */\nconst MIN_API_KEY_LENGTH = 10;\n\n/** ioBroker adapter for parcel.app package tracking */\nclass ParcelappAdapter extends utils.Adapter {\n private client: ParcelClient | null = null;\n private stateManager: StateManager | null = null;\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n private failedDeliveries = new Set<string>();\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n /** ioBroker system language \u2014 read once in `onReady` from `system.config`. EN fallback. */\n private systemLang: string = \"en\";\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n // Wrap async handlers with .catch() so a rejection can never become an\n // unhandled promise rejection (which would SIGKILL the adapter and trap\n // js-controller in a restart loop without any stack trace).\n this.on(\"ready\", () => {\n this.onReady().catch(err => this.log.error(`onReady failed: ${errText(err)}`));\n });\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", obj => {\n this.onMessage(obj).catch(err => this.log.error(`onMessage failed: ${errText(err)}`));\n });\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths (e.g. `void this.poll()`). The per-handler\n // .catch() wrappers cover the documented async paths; this catches\n // anything that slips past during refactors.\n // v0.4.2 (M1): log + terminate(11) instead of leaving the process alive\n // in an undefined state. The per-handler wrappers cover expected paths;\n // anything reaching here is by definition unexpected.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(`Unhandled rejection: ${errText(reason)}`);\n this.terminate?.(11);\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${errText(err)}`);\n this.terminate?.(11);\n };\n process.on(\"unhandledRejection\", this.unhandledRejectionHandler);\n process.on(\"uncaughtException\", this.uncaughtExceptionHandler);\n }\n\n private async onReady(): Promise<void> {\n // Pick the system language up-front so all user-facing logs go out in the\n // user's language. StateManager also gets it for state-name localization.\n const sysConfig = await this.getForeignObjectAsync(\"system.config\");\n const language = (sysConfig?.common as { language?: string } | undefined)?.language ?? \"\";\n if (typeof language === \"string\" && language.length > 0) {\n this.systemLang = language;\n }\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n\n // Validate config\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {\n this.log.error(\"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\");\n return;\n }\n\n // Initialize\n this.client = new ParcelClient(apiKey.trim());\n this.stateManager = new StateManager(this, language);\n\n // Cleanup obsolete states\n await this.cleanupObsoleteStates();\n\n // Initial poll\n await this.poll();\n\n // v0.4.2 (M5): coerce explicitly. Admin can store `pollInterval` as a\n // string; `Math.min(60, \"10\")` happens to coerce, but `Math.max(5,\n // undefined)` returns NaN, and `setInterval(fn, NaN)` becomes\n // `setInterval(fn, 0)` \u2014 a tight loop that hammers the API.\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => void this.poll(), intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n }\n\n /**\n * v0.4.2 (M5+X5): delegate to the shared `coerceClampedInt` helper.\n *\n * @param raw Raw `pollInterval` from admin config (number or numeric string).\n */\n private static coercePollInterval(raw: unknown): number {\n return coerceClampedInt(raw, MIN_POLL_INTERVAL, MAX_POLL_INTERVAL, DEFAULT_POLL_INTERVAL);\n }\n\n private onUnload(callback: () => void): void {\n try {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n // v0.4.2 (M11+P1): cancel every in-flight HTTPS request so a slow\n // parcel.app endpoint doesn't keep the adapter alive past\n // js-controller's 4-second kill deadline.\n this.client?.cancelAll();\n if (this.unhandledRejectionHandler) {\n process.off(\"unhandledRejection\", this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off(\"uncaughtException\", this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n // v0.4.2 (M10): explicit `.catch(() => {})` on the fire-and-forget so\n // a broker-already-down doesn't leak as an unhandled rejection.\n void this.setState(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker is shutting down \u2014 ignore */\n });\n } catch {\n // ignore\n }\n callback();\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n try {\n switch (obj.command) {\n case \"checkConnection\": {\n const msg = obj.message as { apiKey?: string };\n const key = msg?.apiKey?.trim() || \"\";\n if (!key || key.length < MIN_API_KEY_LENGTH) {\n this.sendTo(obj.from, obj.command, { success: false, message: \"API key is too short\" }, obj.callback);\n return;\n }\n const testClient = new ParcelClient(key);\n const result = await testClient.testConnection();\n this.sendTo(obj.from, obj.command, result, obj.callback);\n break;\n }\n case \"addDelivery\": {\n if (!this.client) {\n this.sendTo(\n obj.from,\n obj.command,\n { success: false, error_message: \"Adapter not initialized\" },\n obj.callback,\n );\n return;\n }\n const request = obj.message as {\n tracking_number: string;\n carrier_code: string;\n description: string;\n };\n const addResult = await this.client.addDelivery(request);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (addResult.success) {\n void this.poll();\n }\n break;\n }\n default:\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n this.sendTo(obj.from, obj.command, { success: false, error_message: errText(err) }, obj.callback);\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n const obsoleteStates = [\n \"summary.json\", // removed in 0.2.0\n ];\n for (const stateId of obsoleteStates) {\n const obj = await this.getObjectAsync(stateId);\n if (obj) {\n await this.delObjectAsync(stateId);\n this.log.debug(`Removed obsolete state: ${stateId}`);\n }\n }\n }\n\n /**\n * Classify an error for deduplication and log-level decisions.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code === \"RATE_LIMITED\") {\n return \"RATE_LIMITED\";\n }\n if (error.code === \"INVALID_API_KEY\") {\n return \"INVALID_API_KEY\";\n }\n // v0.4.2 (P3): 403 is a permission issue, distinct from invalid api-key.\n if (error.code === \"FORBIDDEN\") {\n return \"FORBIDDEN\";\n }\n // Network errors: DNS, connection refused, no internet\n if (\n error.code === \"ENOTFOUND\" ||\n error.code === \"ECONNREFUSED\" ||\n error.code === \"ECONNRESET\" ||\n error.code === \"ENETUNREACH\" ||\n error.code === \"EHOSTUNREACH\" ||\n error.code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (error.message.includes(\"timeout\") || error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code || \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n return;\n }\n\n const now = Date.now();\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(`Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`);\n return;\n }\n\n // Throttle: minimum gap between polls\n if (now - this.lastPollTime < MIN_POLL_GAP_MS) {\n this.log.debug(\"Skipping poll \u2014 too soon after last poll\");\n return;\n }\n\n this.isPolling = true;\n this.lastPollTime = now;\n try {\n // When keeping delivered packages, use \"recent\" to get them from API\n const autoRemove = this.config.autoRemoveDelivered !== false;\n const deliveries = await this.client.getDeliveries(autoRemove ? \"active\" : \"recent\");\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n await this.setStateAsync(\"info.connection\", { val: true, ack: true });\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => this.stateManager!.parseStatus(d) !== 0);\n const visibleDeliveries = autoRemove ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset per-poll collision tracker so the bare-id wins\n // for the first occurrence in this poll (deterministic, back-compat).\n this.stateManager.resetPollState();\n\n // v0.4.2 (M4): per-delivery updates run in parallel, each wrapped in\n // try/catch so one bad delivery doesn't poison the others.\n const idResults = await Promise.all(\n visibleDeliveries.map(async delivery => {\n try {\n const carrierName = await this.client!.getCarrierName(delivery.carrier_code);\n await this.stateManager!.updateDelivery(delivery, carrierName);\n this.failedDeliveries.delete(delivery.tracking_number);\n return this.stateManager!.packageId(delivery);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(delivery.tracking_number)) {\n this.log.debug(`Failed to update \"${delivery.tracking_number}\": ${msg}`);\n } else {\n this.log.warn(`Failed to update '${delivery.tracking_number}': ${msg}`);\n this.failedDeliveries.add(delivery.tracking_number);\n }\n return null;\n }\n }),\n );\n const activeIds = idResults.filter((id): id is string => id !== null);\n\n // Cleanup stale deliveries\n await this.stateManager.cleanupDeliveries(activeIds);\n\n // Update summary (always uses active/non-delivered)\n await this.stateManager.updateSummary(activeDeliveries);\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n const error = err as Error & {\n code?: string;\n retryAfterSeconds?: number;\n };\n\n // Classify the error\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n if (error.code === \"RATE_LIMITED\") {\n // v0.4.2 (M9): clamp Retry-After value into [60s, 24h]. A bogus 0,\n // negative, or fractional value used to either wipe the cooldown\n // (set rateLimitedUntil to past) or set it for fractions of a\n // second \u2014 neither is the intended behavior.\n const rawCooldown = error.retryAfterSeconds ?? 0;\n const cooldownSec =\n Number.isFinite(rawCooldown) && rawCooldown > 0\n ? Math.min(24 * 3600, Math.max(60, Math.floor(rawCooldown)))\n : 5 * 60;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n this.log.warn(`Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`);\n } else if (error.code === \"FORBIDDEN\") {\n // v0.4.2 (P3): 403 is a permission issue (e.g. Premium subscription\n // expired). Reauth wouldn't help \u2014 surface a clear hint.\n this.log.error(\n \"parcel.app returned 403 Forbidden \u2014 your account may not have an active Premium subscription, or the API key was revoked. Check your account on parcelapp.net.\",\n );\n } else if (error.code === \"INVALID_API_KEY\") {\n // Always log \u2014 user must fix config\n this.log.error(\"Invalid API key \u2014 please check your parcel.app API key\");\n } else if (isRepeat) {\n // Same error as last time \u2014 don't spam the log\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else if (errorCode === \"NETWORK\") {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n } else if (errorCode === \"TIMEOUT\") {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n } else {\n this.log.error(`Poll failed: ${error.message}`);\n }\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n } finally {\n this.isPolling = false;\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAAA,YAAuB;AACvB,oBAA0C;AAC1C,2BAA6B;AAC7B,2BAA6B;AAE7B,MAAM,oBAAoB;AAC1B,MAAM,oBAAoB;AAC1B,MAAM,wBAAwB;AAC9B,MAAM,kBAAkB;AAExB,MAAM,qBAAqB;AAG3B,MAAM,yBAAyB,MAAM,QAAQ;AAAA,EACnC,SAA8B;AAAA,EAC9B,eAAoC;AAAA,EACpC,YAA2C;AAAA,EAC3C,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,gBAAgB;AAAA,EAChB,mBAAmB,oBAAI,IAAY;AAAA,
|
|
4
|
+
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { coerceClampedInt, errText } from \"./lib/coerce\";\nimport { ParcelClient } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\nconst MIN_POLL_GAP_MS = 60_000; // Minimum 60s between polls\n/** v0.4.2 (M6): minimum length for an apiKey value to even be considered valid. */\nconst MIN_API_KEY_LENGTH = 10;\n\n/** ioBroker adapter for parcel.app package tracking */\nclass ParcelappAdapter extends utils.Adapter {\n private client: ParcelClient | null = null;\n private stateManager: StateManager | null = null;\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n private failedDeliveries = new Set<string>();\n /**\n * v0.4.4: short-lived test-clients spawned from `checkConnection` admin\n * messages. The prod-`this.client` is what `onUnload` cancels, so these\n * need their own registry to be reachable at shutdown. Without this, an\n * admin clicking \"Test Connection\" right before adapter-stop could keep\n * the process alive past js-controller's 4-second kill deadline.\n */\n private testClients = new Set<ParcelClient>();\n private unhandledRejectionHandler: ((reason: unknown) => void) | null = null;\n private uncaughtExceptionHandler: ((err: Error) => void) | null = null;\n /** ioBroker system language \u2014 read once in `onReady` from `system.config`. EN fallback. */\n private systemLang: string = \"en\";\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n // Wrap async handlers with .catch() so a rejection can never become an\n // unhandled promise rejection (which would SIGKILL the adapter and trap\n // js-controller in a restart loop without any stack trace).\n this.on(\"ready\", () => {\n this.onReady().catch(err => this.log.error(`onReady failed: ${errText(err)}`));\n });\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", obj => {\n this.onMessage(obj).catch(err => this.log.error(`onMessage failed: ${errText(err)}`));\n });\n // Last-line-of-defence against unhandled rejections / sync throws from\n // fire-and-forget paths (e.g. `void this.poll()`). The per-handler\n // .catch() wrappers cover the documented async paths; this catches\n // anything that slips past during refactors.\n // v0.4.2 (M1): log + terminate(11) instead of leaving the process alive\n // in an undefined state. The per-handler wrappers cover expected paths;\n // anything reaching here is by definition unexpected.\n this.unhandledRejectionHandler = (reason: unknown) => {\n this.log.error(`Unhandled rejection: ${errText(reason)}`);\n this.terminate?.(11);\n };\n this.uncaughtExceptionHandler = (err: Error) => {\n this.log.error(`Uncaught exception: ${errText(err)}`);\n this.terminate?.(11);\n };\n process.on(\"unhandledRejection\", this.unhandledRejectionHandler);\n process.on(\"uncaughtException\", this.uncaughtExceptionHandler);\n }\n\n private async onReady(): Promise<void> {\n // v0.4.3 (G1): trace start of onReady so the debug log shows when the\n // adapter starts wiring up, what the config inputs look like.\n this.log.debug(\n `onReady: starting (pollInterval=${JSON.stringify(this.config.pollInterval)}, autoRemoveDelivered=${this.config.autoRemoveDelivered})`,\n );\n\n // Pick the system language up-front so all user-facing logs go out in the\n // user's language. StateManager also gets it for state-name localization.\n const sysConfig = await this.getForeignObjectAsync(\"system.config\");\n const language = (sysConfig?.common as { language?: string } | undefined)?.language ?? \"\";\n if (typeof language === \"string\" && language.length > 0) {\n this.systemLang = language;\n }\n // v0.4.3 (G2): trace the resolved system language. Empty/unknown values\n // fall back to \"en\" silently in older versions \u2014 now visible.\n this.log.debug(`system language: '${language}' \u2192 using '${this.systemLang}'`);\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n\n // Validate config\n const { apiKey } = this.config;\n if (!apiKey || apiKey.trim().length < MIN_API_KEY_LENGTH) {\n this.log.error(\"No valid API key configured \u2014 please enter your parcel.app API key in the adapter settings\");\n return;\n }\n\n // Initialize. v0.4.3: pass a debug-logger so the HTTPS layer can trace\n // request/response lifecycle. `loglevel=info` users see nothing changed.\n this.client = new ParcelClient(apiKey.trim(), { debug: (m: string) => this.log.debug(m) });\n this.stateManager = new StateManager(this, language);\n\n // Cleanup obsolete states\n await this.cleanupObsoleteStates();\n\n // Initial poll\n await this.poll();\n\n // v0.4.2 (M5): coerce explicitly. Admin can store `pollInterval` as a\n // string; `Math.min(60, \"10\")` happens to coerce, but `Math.max(5,\n // undefined)` returns NaN, and `setInterval(fn, NaN)` becomes\n // `setInterval(fn, 0)` \u2014 a tight loop that hammers the API.\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\n // v0.4.3 (G3): always-log the resolved interval. Detecting \"drift\" from\n // the raw value is fragile (`\"10\"` \u2192 `10` is not drift, just coercion);\n // an always-on log line keeps the resolution visible without false-flag\n // logic. See `Ressourcen/parcelapp/v0.4.3-debug-audit.md` advisor pt 3.\n this.log.debug(`pollInterval: raw=${JSON.stringify(this.config.pollInterval)} resolved=${interval}min`);\n const intervalMs = interval * 60 * 1000;\n this.pollTimer = this.setInterval(() => void this.poll(), intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n }\n\n /**\n * v0.4.2 (M5+X5): delegate to the shared `coerceClampedInt` helper.\n *\n * @param raw Raw `pollInterval` from admin config (number or numeric string).\n */\n private static coercePollInterval(raw: unknown): number {\n return coerceClampedInt(raw, MIN_POLL_INTERVAL, MAX_POLL_INTERVAL, DEFAULT_POLL_INTERVAL);\n }\n\n private onUnload(callback: () => void): void {\n try {\n if (this.pollTimer) {\n this.clearInterval(this.pollTimer);\n this.pollTimer = undefined;\n }\n // v0.4.2 (M11+P1): cancel every in-flight HTTPS request so a slow\n // parcel.app endpoint doesn't keep the adapter alive past\n // js-controller's 4-second kill deadline.\n this.client?.cancelAll();\n // v0.4.4: also abort any short-lived test-client (from checkConnection)\n // whose HTTPS-request might still be inflight at shutdown \u2014 the prod\n // `this.client.cancelAll()` only touches the production-client.\n for (const tc of this.testClients) {\n tc.cancelAll();\n }\n this.testClients.clear();\n if (this.unhandledRejectionHandler) {\n process.off(\"unhandledRejection\", this.unhandledRejectionHandler);\n this.unhandledRejectionHandler = null;\n }\n if (this.uncaughtExceptionHandler) {\n process.off(\"uncaughtException\", this.uncaughtExceptionHandler);\n this.uncaughtExceptionHandler = null;\n }\n // v0.4.2 (M10): explicit `.catch(() => {})` on the fire-and-forget so\n // a broker-already-down doesn't leak as an unhandled rejection.\n void this.setState(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker is shutting down \u2014 ignore */\n });\n } catch (err) {\n // v0.4.3 (G4): replace silent `// ignore` with a trace so shutdown\n // errors leave a debug breadcrumb. Broker-already-down errors here\n // are expected \u2014 debug-level keeps them out of the user log.\n this.log.debug(`onUnload error (ignored): ${errText(err)}`);\n }\n callback();\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n // v0.4.3 (F1): entry log BEFORE the early-return \u2014 broadcast messages\n // without callback wouldn't be visible otherwise.\n this.log.debug(`onMessage: command='${obj?.command}' from='${obj?.from}' has-callback=${!!obj?.callback}`);\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n try {\n switch (obj.command) {\n case \"checkConnection\": {\n const msg = obj.message as { apiKey?: string };\n const key = msg?.apiKey?.trim() || \"\";\n if (!key || key.length < MIN_API_KEY_LENGTH) {\n // v0.4.3 (F2): trace the reject before sendTo.\n this.log.debug(\"checkConnection: apiKey too short\");\n this.sendTo(obj.from, obj.command, { success: false, message: \"API key is too short\" }, obj.callback);\n return;\n }\n // v0.4.3: same debug-logger as the prod client so checkConnection\n // failures get the same HTTPS-layer trace.\n const testClient = new ParcelClient(key, { debug: (m: string) => this.log.debug(m) });\n // v0.4.4: register test-client so onUnload can abort its inflight\n // HTTPS-request \u2014 the adapter's `this.client.cancelAll()` only\n // touches the prod-client, not these short-lived test-clients.\n this.testClients.add(testClient);\n try {\n const result = await testClient.testConnection();\n // v0.4.3 (F3): trace checkConnection result.\n this.log.debug(`checkConnection: result=${result.success ? \"ok\" : \"fail\"} (${result.message})`);\n this.sendTo(obj.from, obj.command, result, obj.callback);\n } finally {\n this.testClients.delete(testClient);\n }\n break;\n }\n case \"addDelivery\": {\n if (!this.client) {\n // v0.4.3 (F4): trace addDelivery-before-init.\n this.log.debug(\"addDelivery: adapter not initialized\");\n this.sendTo(\n obj.from,\n obj.command,\n { success: false, error_message: \"Adapter not initialized\" },\n obj.callback,\n );\n return;\n }\n const request = obj.message as {\n tracking_number: string;\n carrier_code: string;\n description: string;\n };\n const addResult = await this.client.addDelivery(request);\n // v0.4.3 (F5): trace addDelivery result with the tracking number.\n this.log.debug(`addDelivery: '${request?.tracking_number}' result=${addResult.success ? \"ok\" : \"fail\"}`);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (addResult.success) {\n void this.poll();\n }\n break;\n }\n default:\n // v0.4.3 (F6): trace unknown command before sendTo.\n this.log.debug(`onMessage: unknown command '${obj.command}'`);\n this.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n }\n } catch (err) {\n // v0.4.3 (F7): trace catch so the debug log shows what failed.\n // The sendTo back to the caller is preserved unchanged.\n this.log.debug(`onMessage: '${obj.command}' failed: ${errText(err)}`);\n this.sendTo(obj.from, obj.command, { success: false, error_message: errText(err) }, obj.callback);\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n const obsoleteStates = [\n \"summary.json\", // removed in 0.2.0\n ];\n for (const stateId of obsoleteStates) {\n const obj = await this.getObjectAsync(stateId);\n if (obj) {\n await this.delObjectAsync(stateId);\n this.log.debug(`Removed obsolete state: ${stateId}`);\n }\n }\n }\n\n /**\n * Classify an error for deduplication and log-level decisions.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code === \"RATE_LIMITED\") {\n return \"RATE_LIMITED\";\n }\n if (error.code === \"INVALID_API_KEY\") {\n return \"INVALID_API_KEY\";\n }\n // v0.4.2 (P3): 403 is a permission issue, distinct from invalid api-key.\n if (error.code === \"FORBIDDEN\") {\n return \"FORBIDDEN\";\n }\n // Network errors: DNS, connection refused, no internet\n if (\n error.code === \"ENOTFOUND\" ||\n error.code === \"ECONNREFUSED\" ||\n error.code === \"ECONNRESET\" ||\n error.code === \"ENETUNREACH\" ||\n error.code === \"EHOSTUNREACH\" ||\n error.code === \"EAI_AGAIN\"\n ) {\n return \"NETWORK\";\n }\n if (error.message.includes(\"timeout\") || error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code || \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n return;\n }\n\n const now = Date.now();\n // v0.4.3 (B1): poll-entry anchor \u2014 visible after the re-entry guard but\n // before the rate-limit/throttle skips. Shows mode + current error state\n // so the debug log gives context for whatever follows.\n const autoRemoveMode = this.config.autoRemoveDelivered !== false;\n this.log.debug(`poll: starting (autoRemove=${autoRemoveMode}, lastErrorCode='${this.lastErrorCode}')`);\n\n // Skip if rate limited\n if (now < this.rateLimitedUntil) {\n const waitMin = Math.ceil((this.rateLimitedUntil - now) / 60_000);\n this.log.debug(`Skipping poll \u2014 rate limited for ${waitMin} more minute(s)`);\n return;\n }\n\n // Throttle: minimum gap between polls\n if (now - this.lastPollTime < MIN_POLL_GAP_MS) {\n this.log.debug(\"Skipping poll \u2014 too soon after last poll\");\n return;\n }\n\n this.isPolling = true;\n this.lastPollTime = now;\n try {\n // When keeping delivered packages, use \"recent\" to get them from API\n const deliveries = await this.client.getDeliveries(autoRemoveMode ? \"active\" : \"recent\");\n\n // Reset error state on success\n this.rateLimitedUntil = 0;\n if (this.lastErrorCode) {\n this.log.info(\"Connection restored\");\n this.lastErrorCode = \"\";\n }\n await this.setStateAsync(\"info.connection\", { val: true, ack: true });\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => this.stateManager!.parseStatus(d) !== 0);\n const visibleDeliveries = autoRemoveMode ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset per-poll collision tracker so the bare-id wins\n // for the first occurrence in this poll (deterministic, back-compat).\n this.stateManager.resetPollState();\n\n // v0.4.2 (M4): per-delivery updates run in parallel, each wrapped in\n // try/catch so one bad delivery doesn't poison the others.\n const idResults = await Promise.all(\n visibleDeliveries.map(async delivery => {\n try {\n // v0.4.3 (C1): per-delivery entry. ~10 packages \u00D7 144 polls/day\n // = ~1440 debug lines/day \u2014 acceptable at debug-level. Line stays\n // short (tracking + carrier + status only, no full delivery JSON).\n this.log.debug(\n `updateDelivery: '${delivery.tracking_number}' carrier=${delivery.carrier_code} status=${delivery.status_code}`,\n );\n const carrierName = await this.client!.getCarrierName(delivery.carrier_code);\n await this.stateManager!.updateDelivery(delivery, carrierName);\n this.failedDeliveries.delete(delivery.tracking_number);\n return this.stateManager!.packageId(delivery);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(delivery.tracking_number)) {\n this.log.debug(`Failed to update \"${delivery.tracking_number}\": ${msg}`);\n } else {\n this.log.warn(`Failed to update '${delivery.tracking_number}': ${msg}`);\n this.failedDeliveries.add(delivery.tracking_number);\n }\n return null;\n }\n }),\n );\n const activeIds = idResults.filter((id): id is string => id !== null);\n\n // Cleanup stale deliveries\n await this.stateManager.cleanupDeliveries(activeIds);\n\n // Update summary (always uses active/non-delivered)\n await this.stateManager.updateSummary(activeDeliveries);\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n const error = err as Error & {\n code?: string;\n retryAfterSeconds?: number;\n };\n\n // Classify the error\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n if (error.code === \"RATE_LIMITED\") {\n // v0.4.2 (M9): clamp Retry-After value into [60s, 24h]. A bogus 0,\n // negative, or fractional value used to either wipe the cooldown\n // (set rateLimitedUntil to past) or set it for fractions of a\n // second \u2014 neither is the intended behavior.\n const rawCooldown = error.retryAfterSeconds ?? 0;\n const cooldownSec =\n Number.isFinite(rawCooldown) && rawCooldown > 0\n ? Math.min(24 * 3600, Math.max(60, Math.floor(rawCooldown)))\n : 5 * 60;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n this.log.warn(`Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`);\n } else if (error.code === \"FORBIDDEN\") {\n // v0.4.2 (P3): 403 is a permission issue (e.g. Premium subscription\n // expired). Reauth wouldn't help \u2014 surface a clear hint.\n this.log.error(\n \"parcel.app returned 403 Forbidden \u2014 your account may not have an active Premium subscription, or the API key was revoked. Check your account on parcelapp.net.\",\n );\n } else if (error.code === \"INVALID_API_KEY\") {\n // Always log \u2014 user must fix config\n this.log.error(\"Invalid API key \u2014 please check your parcel.app API key\");\n } else if (isRepeat) {\n // Same error as last time \u2014 don't spam the log\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else if (errorCode === \"NETWORK\") {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n } else if (errorCode === \"TIMEOUT\") {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n } else {\n this.log.error(`Poll failed: ${error.message}`);\n }\n\n await this.setStateAsync(\"info.connection\", { val: false, ack: true });\n } finally {\n this.isPolling = false;\n }\n }\n}\n\nif (require.main !== module) {\n module.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ParcelappAdapter(options);\n} else {\n (() => new ParcelappAdapter())();\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAAA,YAAuB;AACvB,oBAA0C;AAC1C,2BAA6B;AAC7B,2BAA6B;AAE7B,MAAM,oBAAoB;AAC1B,MAAM,oBAAoB;AAC1B,MAAM,wBAAwB;AAC9B,MAAM,kBAAkB;AAExB,MAAM,qBAAqB;AAG3B,MAAM,yBAAyB,MAAM,QAAQ;AAAA,EACnC,SAA8B;AAAA,EAC9B,eAAoC;AAAA,EACpC,YAA2C;AAAA,EAC3C,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,gBAAgB;AAAA,EAChB,mBAAmB,oBAAI,IAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQnC,cAAc,oBAAI,IAAkB;AAAA,EACpC,4BAAgE;AAAA,EAChE,2BAA0D;AAAA;AAAA,EAE1D,aAAqB;AAAA;AAAA,EAGtB,YAAY,UAAyC,CAAC,GAAG;AAC9D,UAAM;AAAA,MACJ,GAAG;AAAA,MACH,MAAM;AAAA,IACR,CAAC;AAID,SAAK,GAAG,SAAS,MAAM;AACrB,WAAK,QAAQ,EAAE,MAAM,SAAO,KAAK,IAAI,MAAM,uBAAmB,uBAAQ,GAAG,CAAC,EAAE,CAAC;AAAA,IAC/E,CAAC;AACD,SAAK,GAAG,UAAU,KAAK,SAAS,KAAK,IAAI,CAAC;AAC1C,SAAK,GAAG,WAAW,SAAO;AACxB,WAAK,UAAU,GAAG,EAAE,MAAM,SAAO,KAAK,IAAI,MAAM,yBAAqB,uBAAQ,GAAG,CAAC,EAAE,CAAC;AAAA,IACtF,CAAC;AAQD,SAAK,4BAA4B,CAAC,WAAoB;AA1D1D;AA2DM,WAAK,IAAI,MAAM,4BAAwB,uBAAQ,MAAM,CAAC,EAAE;AACxD,iBAAK,cAAL,8BAAiB;AAAA,IACnB;AACA,SAAK,2BAA2B,CAAC,QAAe;AA9DpD;AA+DM,WAAK,IAAI,MAAM,2BAAuB,uBAAQ,GAAG,CAAC,EAAE;AACpD,iBAAK,cAAL,8BAAiB;AAAA,IACnB;AACA,YAAQ,GAAG,sBAAsB,KAAK,yBAAyB;AAC/D,YAAQ,GAAG,qBAAqB,KAAK,wBAAwB;AAAA,EAC/D;AAAA,EAEA,MAAc,UAAyB;AAtEzC;AAyEI,SAAK,IAAI;AAAA,MACP,mCAAmC,KAAK,UAAU,KAAK,OAAO,YAAY,CAAC,yBAAyB,KAAK,OAAO,mBAAmB;AAAA,IACrI;AAIA,UAAM,YAAY,MAAM,KAAK,sBAAsB,eAAe;AAClE,UAAM,YAAY,kDAAW,WAAX,mBAAyD,aAAzD,YAAqE;AACvF,QAAI,OAAO,aAAa,YAAY,SAAS,SAAS,GAAG;AACvD,WAAK,aAAa;AAAA,IACpB;AAGA,SAAK,IAAI,MAAM,qBAAqB,QAAQ,mBAAc,KAAK,UAAU,GAAG;AAE5E,UAAM,KAAK,cAAc,mBAAmB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAGrE,UAAM,EAAE,OAAO,IAAI,KAAK;AACxB,QAAI,CAAC,UAAU,OAAO,KAAK,EAAE,SAAS,oBAAoB;AACxD,WAAK,IAAI,MAAM,iGAA4F;AAC3G;AAAA,IACF;AAIA,SAAK,SAAS,IAAI,kCAAa,OAAO,KAAK,GAAG,EAAE,OAAO,CAAC,MAAc,KAAK,IAAI,MAAM,CAAC,EAAE,CAAC;AACzF,SAAK,eAAe,IAAI,kCAAa,MAAM,QAAQ;AAGnD,UAAM,KAAK,sBAAsB;AAGjC,UAAM,KAAK,KAAK;AAMhB,UAAM,WAAW,iBAAiB,mBAAmB,KAAK,OAAO,YAAY;AAK7E,SAAK,IAAI,MAAM,qBAAqB,KAAK,UAAU,KAAK,OAAO,YAAY,CAAC,aAAa,QAAQ,KAAK;AACtG,UAAM,aAAa,WAAW,KAAK;AACnC,SAAK,YAAY,KAAK,YAAY,MAAM,KAAK,KAAK,KAAK,GAAG,UAAU;AAEpE,SAAK,IAAI,KAAK,gDAA2C,QAAQ,UAAU;AAAA,EAC7E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAe,mBAAmB,KAAsB;AACtD,eAAO,gCAAiB,KAAK,mBAAmB,mBAAmB,qBAAqB;AAAA,EAC1F;AAAA,EAEQ,SAAS,UAA4B;AArI/C;AAsII,QAAI;AACF,UAAI,KAAK,WAAW;AAClB,aAAK,cAAc,KAAK,SAAS;AACjC,aAAK,YAAY;AAAA,MACnB;AAIA,iBAAK,WAAL,mBAAa;AAIb,iBAAW,MAAM,KAAK,aAAa;AACjC,WAAG,UAAU;AAAA,MACf;AACA,WAAK,YAAY,MAAM;AACvB,UAAI,KAAK,2BAA2B;AAClC,gBAAQ,IAAI,sBAAsB,KAAK,yBAAyB;AAChE,aAAK,4BAA4B;AAAA,MACnC;AACA,UAAI,KAAK,0BAA0B;AACjC,gBAAQ,IAAI,qBAAqB,KAAK,wBAAwB;AAC9D,aAAK,2BAA2B;AAAA,MAClC;AAGA,WAAK,KAAK,SAAS,mBAAmB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,MAE7E,CAAC;AAAA,IACH,SAAS,KAAK;AAIZ,WAAK,IAAI,MAAM,iCAA6B,uBAAQ,GAAG,CAAC,EAAE;AAAA,IAC5D;AACA,aAAS;AAAA,EACX;AAAA,EAEA,MAAc,UAAU,KAAsC;AA5KhE;AA+KI,SAAK,IAAI,MAAM,uBAAuB,2BAAK,OAAO,WAAW,2BAAK,IAAI,kBAAkB,CAAC,EAAC,2BAAK,SAAQ,EAAE;AACzG,QAAI,EAAC,2BAAK,YAAW,CAAC,IAAI,UAAU;AAClC;AAAA,IACF;AAEA,QAAI;AACF,cAAQ,IAAI,SAAS;AAAA,QACnB,KAAK,mBAAmB;AACtB,gBAAM,MAAM,IAAI;AAChB,gBAAM,QAAM,gCAAK,WAAL,mBAAa,WAAU;AACnC,cAAI,CAAC,OAAO,IAAI,SAAS,oBAAoB;AAE3C,iBAAK,IAAI,MAAM,mCAAmC;AAClD,iBAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,SAAS,OAAO,SAAS,uBAAuB,GAAG,IAAI,QAAQ;AACpG;AAAA,UACF;AAGA,gBAAM,aAAa,IAAI,kCAAa,KAAK,EAAE,OAAO,CAAC,MAAc,KAAK,IAAI,MAAM,CAAC,EAAE,CAAC;AAIpF,eAAK,YAAY,IAAI,UAAU;AAC/B,cAAI;AACF,kBAAM,SAAS,MAAM,WAAW,eAAe;AAE/C,iBAAK,IAAI,MAAM,2BAA2B,OAAO,UAAU,OAAO,MAAM,KAAK,OAAO,OAAO,GAAG;AAC9F,iBAAK,OAAO,IAAI,MAAM,IAAI,SAAS,QAAQ,IAAI,QAAQ;AAAA,UACzD,UAAE;AACA,iBAAK,YAAY,OAAO,UAAU;AAAA,UACpC;AACA;AAAA,QACF;AAAA,QACA,KAAK,eAAe;AAClB,cAAI,CAAC,KAAK,QAAQ;AAEhB,iBAAK,IAAI,MAAM,sCAAsC;AACrD,iBAAK;AAAA,cACH,IAAI;AAAA,cACJ,IAAI;AAAA,cACJ,EAAE,SAAS,OAAO,eAAe,0BAA0B;AAAA,cAC3D,IAAI;AAAA,YACN;AACA;AAAA,UACF;AACA,gBAAM,UAAU,IAAI;AAKpB,gBAAM,YAAY,MAAM,KAAK,OAAO,YAAY,OAAO;AAEvD,eAAK,IAAI,MAAM,iBAAiB,mCAAS,eAAe,YAAY,UAAU,UAAU,OAAO,MAAM,EAAE;AACvG,eAAK,OAAO,IAAI,MAAM,IAAI,SAAS,WAAW,IAAI,QAAQ;AAC1D,cAAI,UAAU,SAAS;AACrB,iBAAK,KAAK,KAAK;AAAA,UACjB;AACA;AAAA,QACF;AAAA,QACA;AAEE,eAAK,IAAI,MAAM,+BAA+B,IAAI,OAAO,GAAG;AAC5D,eAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,OAAO,kBAAkB,GAAG,IAAI,QAAQ;AAAA,MACjF;AAAA,IACF,SAAS,KAAK;AAGZ,WAAK,IAAI,MAAM,eAAe,IAAI,OAAO,iBAAa,uBAAQ,GAAG,CAAC,EAAE;AACpE,WAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,SAAS,OAAO,mBAAe,uBAAQ,GAAG,EAAE,GAAG,IAAI,QAAQ;AAAA,IAClG;AAAA,EACF;AAAA,EAEA,MAAc,wBAAuC;AACnD,UAAM,iBAAiB;AAAA,MACrB;AAAA;AAAA,IACF;AACA,eAAW,WAAW,gBAAgB;AACpC,YAAM,MAAM,MAAM,KAAK,eAAe,OAAO;AAC7C,UAAI,KAAK;AACP,cAAM,KAAK,eAAe,OAAO;AACjC,aAAK,IAAI,MAAM,2BAA2B,OAAO,EAAE;AAAA,MACrD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,cAAc,OAA0C;AAC9D,QAAI,MAAM,SAAS,gBAAgB;AACjC,aAAO;AAAA,IACT;AACA,QAAI,MAAM,SAAS,mBAAmB;AACpC,aAAO;AAAA,IACT;AAEA,QAAI,MAAM,SAAS,aAAa;AAC9B,aAAO;AAAA,IACT;AAEA,QACE,MAAM,SAAS,eACf,MAAM,SAAS,kBACf,MAAM,SAAS,gBACf,MAAM,SAAS,iBACf,MAAM,SAAS,kBACf,MAAM,SAAS,aACf;AACA,aAAO;AAAA,IACT;AACA,QAAI,MAAM,QAAQ,SAAS,SAAS,KAAK,MAAM,SAAS,aAAa;AACnE,aAAO;AAAA,IACT;AACA,WAAO,MAAM,QAAQ;AAAA,EACvB;AAAA,EAEA,MAAc,OAAsB;AArStC;AAsSI,QAAI,KAAK,aAAa,CAAC,KAAK,UAAU,CAAC,KAAK,cAAc;AACxD;AAAA,IACF;AAEA,UAAM,MAAM,KAAK,IAAI;AAIrB,UAAM,iBAAiB,KAAK,OAAO,wBAAwB;AAC3D,SAAK,IAAI,MAAM,8BAA8B,cAAc,oBAAoB,KAAK,aAAa,IAAI;AAGrG,QAAI,MAAM,KAAK,kBAAkB;AAC/B,YAAM,UAAU,KAAK,MAAM,KAAK,mBAAmB,OAAO,GAAM;AAChE,WAAK,IAAI,MAAM,yCAAoC,OAAO,iBAAiB;AAC3E;AAAA,IACF;AAGA,QAAI,MAAM,KAAK,eAAe,iBAAiB;AAC7C,WAAK,IAAI,MAAM,+CAA0C;AACzD;AAAA,IACF;AAEA,SAAK,YAAY;AACjB,SAAK,eAAe;AACpB,QAAI;AAEF,YAAM,aAAa,MAAM,KAAK,OAAO,cAAc,iBAAiB,WAAW,QAAQ;AAGvF,WAAK,mBAAmB;AACxB,UAAI,KAAK,eAAe;AACtB,aAAK,IAAI,KAAK,qBAAqB;AACnC,aAAK,gBAAgB;AAAA,MACvB;AACA,YAAM,KAAK,cAAc,mBAAmB,EAAE,KAAK,MAAM,KAAK,KAAK,CAAC;AAGpE,YAAM,mBAAmB,WAAW,OAAO,OAAK,KAAK,aAAc,YAAY,CAAC,MAAM,CAAC;AACvF,YAAM,oBAAoB,iBAAiB,mBAAmB;AAI9D,WAAK,aAAa,eAAe;AAIjC,YAAM,YAAY,MAAM,QAAQ;AAAA,QAC9B,kBAAkB,IAAI,OAAM,aAAY;AACtC,cAAI;AAIF,iBAAK,IAAI;AAAA,cACP,oBAAoB,SAAS,eAAe,aAAa,SAAS,YAAY,WAAW,SAAS,WAAW;AAAA,YAC/G;AACA,kBAAM,cAAc,MAAM,KAAK,OAAQ,eAAe,SAAS,YAAY;AAC3E,kBAAM,KAAK,aAAc,eAAe,UAAU,WAAW;AAC7D,iBAAK,iBAAiB,OAAO,SAAS,eAAe;AACrD,mBAAO,KAAK,aAAc,UAAU,QAAQ;AAAA,UAC9C,SAAS,KAAK;AACZ,kBAAM,UAAM,uBAAQ,GAAG;AACvB,gBAAI,KAAK,iBAAiB,IAAI,SAAS,eAAe,GAAG;AACvD,mBAAK,IAAI,MAAM,qBAAqB,SAAS,eAAe,MAAM,GAAG,EAAE;AAAA,YACzE,OAAO;AACL,mBAAK,IAAI,KAAK,qBAAqB,SAAS,eAAe,MAAM,GAAG,EAAE;AACtE,mBAAK,iBAAiB,IAAI,SAAS,eAAe;AAAA,YACpD;AACA,mBAAO;AAAA,UACT;AAAA,QACF,CAAC;AAAA,MACH;AACA,YAAM,YAAY,UAAU,OAAO,CAAC,OAAqB,OAAO,IAAI;AAGpE,YAAM,KAAK,aAAa,kBAAkB,SAAS;AAGnD,YAAM,KAAK,aAAa,cAAc,gBAAgB;AAEtD,WAAK,IAAI,MAAM,UAAU,kBAAkB,MAAM,gBAAgB,iBAAiB,MAAM,UAAU;AAAA,IACpG,SAAS,KAAK;AACZ,YAAM,QAAQ;AAMd,YAAM,YAAY,KAAK,cAAc,KAAK;AAC1C,YAAM,WAAW,cAAc,KAAK;AACpC,WAAK,gBAAgB;AAErB,UAAI,MAAM,SAAS,gBAAgB;AAKjC,cAAM,eAAc,WAAM,sBAAN,YAA2B;AAC/C,cAAM,cACJ,OAAO,SAAS,WAAW,KAAK,cAAc,IAC1C,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI,IAAI,KAAK,MAAM,WAAW,CAAC,CAAC,IACzD,IAAI;AACV,aAAK,mBAAmB,KAAK,IAAI,IAAI,cAAc;AACnD,aAAK,IAAI,KAAK,kDAA6C,KAAK,KAAK,cAAc,EAAE,CAAC,YAAY;AAAA,MACpG,WAAW,MAAM,SAAS,aAAa;AAGrC,aAAK,IAAI;AAAA,UACP;AAAA,QACF;AAAA,MACF,WAAW,MAAM,SAAS,mBAAmB;AAE3C,aAAK,IAAI,MAAM,6DAAwD;AAAA,MACzE,WAAW,UAAU;AAEnB,aAAK,IAAI,MAAM,0BAA0B,MAAM,OAAO,EAAE;AAAA,MAC1D,WAAW,cAAc,WAAW;AAClC,aAAK,IAAI,KAAK,uDAAkD;AAAA,MAClE,WAAW,cAAc,WAAW;AAClC,aAAK,IAAI,KAAK,kDAA6C;AAAA,MAC7D,OAAO;AACL,aAAK,IAAI,MAAM,gBAAgB,MAAM,OAAO,EAAE;AAAA,MAChD;AAEA,YAAM,KAAK,cAAc,mBAAmB,EAAE,KAAK,OAAO,KAAK,KAAK,CAAC;AAAA,IACvE,UAAE;AACA,WAAK,YAAY;AAAA,IACnB;AAAA,EACF;AACF;AAEA,IAAI,QAAQ,SAAS,QAAQ;AAC3B,SAAO,UAAU,CAAC,YAAuD,IAAI,iBAAiB,OAAO;AACvG,OAAO;AACL,GAAC,MAAM,IAAI,iBAAiB,GAAG;AACjC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "parcelapp",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.4.4": {
|
|
7
|
+
"en": "Adapter shuts down cleanly even if Test Connection was still running — the test request is now aborted at unload.",
|
|
8
|
+
"de": "Adapter fährt sauber herunter, auch wenn Test Connection noch lief — die Test-Anfrage wird jetzt beim Unload abgebrochen.",
|
|
9
|
+
"ru": "Адаптер чисто завершает работу даже если кнопка Test Connection ещё выполнялась — тестовый запрос теперь прерывается при выгрузке.",
|
|
10
|
+
"pt": "O adaptador encerra corretamente mesmo se o botão Test Connection ainda estava em execução — o pedido de teste é agora abortado no unload.",
|
|
11
|
+
"nl": "De adapter sluit netjes af, ook als Test Connection nog liep — het testverzoek wordt nu bij unload afgebroken.",
|
|
12
|
+
"fr": "L'adaptateur s'arrête proprement même si Test Connection était encore en cours — la requête de test est maintenant interrompue au déchargement.",
|
|
13
|
+
"it": "L'adattatore si arresta correttamente anche se Test Connection era ancora in esecuzione — la richiesta di test viene ora interrotta allo scarico.",
|
|
14
|
+
"es": "El adaptador se cierra correctamente incluso si Test Connection seguía ejecutándose — la solicitud de prueba se aborta ahora al descargar.",
|
|
15
|
+
"pl": "Adapter zamyka się poprawnie nawet jeśli przycisk Test Connection wciąż działał — żądanie testowe jest teraz przerywane przy zwolnieniu.",
|
|
16
|
+
"uk": "Адаптер коректно завершує роботу навіть якщо Test Connection ще виконувався — тестовий запит тепер скасовується під час вивантаження.",
|
|
17
|
+
"zh-cn": "即使测试连接按钮仍在运行,适配器也能干净地关闭——测试请求现在会在卸载时中止。"
|
|
18
|
+
},
|
|
19
|
+
"0.4.3": {
|
|
20
|
+
"en": "Debug log traces previously silent paths: HTTPS request lifecycle, carrier-list fetch outcome, per-delivery updates, admin-message handling. Default log unchanged.",
|
|
21
|
+
"de": "Debug-Log dokumentiert stille Pfade: HTTPS-Request-Lifecycle, Carrier-Liste, Per-Paket-Updates und Admin-Message-Handling. Default-Log unverändert.",
|
|
22
|
+
"ru": "Журнал отладки прослеживает тихие пути: цикл HTTPS-запроса, получение списка перевозчиков, обновления посылок и обработка сообщений. Стандартный лог не изменился.",
|
|
23
|
+
"pt": "Log de depuração rastreia caminhos silenciosos: ciclo HTTPS, lista de transportadoras, atualizações por encomenda e mensagens admin. Log padrão inalterado.",
|
|
24
|
+
"nl": "Debug-log volgt stille paden: HTTPS-cyclus, vervoerderslijst, pakketupdates en admin-berichten. Standaard log ongewijzigd.",
|
|
25
|
+
"fr": "Le journal de débogage trace les chemins silencieux : cycle HTTPS, liste des transporteurs, mises à jour de colis et messages admin. Journal par défaut inchangé.",
|
|
26
|
+
"it": "Log di debug traccia percorsi silenziosi: ciclo HTTPS, elenco corrieri, aggiornamenti pacchi e messaggi admin. Log predefinito invariato.",
|
|
27
|
+
"es": "Log de depuración rastrea rutas silenciosas: ciclo HTTPS, lista de transportistas, actualizaciones de paquetes y mensajes de admin. Log por defecto sin cambios.",
|
|
28
|
+
"pl": "Log debugowania śledzi ciche ścieżki: cykl HTTPS, lista przewoźników, aktualizacje paczek i wiadomości admin. Domyślny log bez zmian.",
|
|
29
|
+
"uk": "Журнал зневадження відстежує тихі шляхи: цикл HTTPS, список перевізників, оновлення посилок та повідомлення адміна. Стандартний лог без змін.",
|
|
30
|
+
"zh-cn": "调试日志跟踪以前静默的路径:HTTPS 请求生命周期、承运商列表、每个包裹更新以及管理消息处理。默认日志保持不变。"
|
|
31
|
+
},
|
|
6
32
|
"0.4.2": {
|
|
7
33
|
"en": "Adapter shuts down cleanly when parcel.app is slow, 403 Forbidden gives a 'check Premium' hint instead of reauth-loop, two trackings with the same sanitized id no longer overwrite.",
|
|
8
34
|
"de": "Adapter beendet sauber wenn parcel.app langsam ist, 403-Fehler liefert 'Premium prüfen'-Hinweis statt Reauth-Loop, und zwei Trackings mit identischer Sanitize-ID überschreiben sich nicht mehr.",
|
|
@@ -67,32 +93,6 @@
|
|
|
67
93
|
"pl": "Czyszczenie dokumentacji. Bez zmian w kodzie.",
|
|
68
94
|
"uk": "Очищення документації. Без змін у коді.",
|
|
69
95
|
"zh-cn": "文档清理。代码无变化。"
|
|
70
|
-
},
|
|
71
|
-
"0.3.0": {
|
|
72
|
-
"en": "Internal cleanup. No user-facing changes.",
|
|
73
|
-
"de": "Interne Bereinigung. Keine User-sichtbaren Änderungen.",
|
|
74
|
-
"ru": "Внутренняя очистка. Без видимых изменений для пользователя.",
|
|
75
|
-
"pt": "Limpeza interna. Sem alterações visíveis para o usuário.",
|
|
76
|
-
"nl": "Interne opschoning. Geen wijzigingen voor de gebruiker.",
|
|
77
|
-
"fr": "Nettoyage interne. Aucune modification visible pour l'utilisateur.",
|
|
78
|
-
"it": "Pulizia interna. Nessuna modifica visibile all'utente.",
|
|
79
|
-
"es": "Limpieza interna. Sin cambios visibles para el usuario.",
|
|
80
|
-
"pl": "Wewnętrzne porządki. Brak zmian widocznych dla użytkownika.",
|
|
81
|
-
"uk": "Внутрішнє прибирання. Без помітних для користувача змін.",
|
|
82
|
-
"zh-cn": "内部清理。对用户无可见变化。"
|
|
83
|
-
},
|
|
84
|
-
"0.2.18": {
|
|
85
|
-
"en": "Internal cleanup. No user-facing changes.",
|
|
86
|
-
"de": "Interne Bereinigung. Keine User-sichtbaren Änderungen.",
|
|
87
|
-
"ru": "Внутренняя очистка. Без видимых изменений для пользователя.",
|
|
88
|
-
"pt": "Limpeza interna. Sem alterações visíveis para o usuário.",
|
|
89
|
-
"nl": "Interne opschoning. Geen wijzigingen voor de gebruiker.",
|
|
90
|
-
"fr": "Nettoyage interne. Aucune modification visible pour l'utilisateur.",
|
|
91
|
-
"it": "Pulizia interna. Nessuna modifica visibile all'utente.",
|
|
92
|
-
"es": "Limpieza interna. Sin cambios visibles para el usuario.",
|
|
93
|
-
"pl": "Wewnętrzne porządki. Brak zmian widocznych dla użytkownika.",
|
|
94
|
-
"uk": "Внутрішнє прибирання. Без помітних для користувача змін.",
|
|
95
|
-
"zh-cn": "内部清理。对用户无可见变化。"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|