iobroker.parcelapp 0.10.0 → 0.10.1
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 +4 -4
- package/build/lib/state-manager.js +33 -5
- package/build/lib/state-manager.js.map +3 -3
- package/build/main.js +14 -0
- package/build/main.js.map +2 -2
- package/io-package.json +14 -14
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -140,6 +140,10 @@ sendTo("parcelapp.0", "addDelivery", {
|
|
|
140
140
|
Placeholder for the next version (at the beginning of the line):
|
|
141
141
|
### **WORK IN PROGRESS**
|
|
142
142
|
-->
|
|
143
|
+
### 0.10.1 (2026-07-13)
|
|
144
|
+
|
|
145
|
+
- Internal refactoring. No user-facing changes.
|
|
146
|
+
|
|
143
147
|
### 0.10.0 (2026-07-08)
|
|
144
148
|
|
|
145
149
|
- Fixed: the admin "Test Connection" button now reports real failures — before, it always showed "Ok" even with a wrong API key.
|
|
@@ -166,10 +170,6 @@ sendTo("parcelapp.0", "addDelivery", {
|
|
|
166
170
|
- Much quieter state updates: a package's last-updated timestamp now only changes when its tracking data actually changed, and device entries are no longer rewritten on every poll
|
|
167
171
|
- Adding a delivery with a malformed request now returns a clear error message instead of failing cryptically
|
|
168
172
|
|
|
169
|
-
### 0.7.1 (2026-06-09)
|
|
170
|
-
|
|
171
|
-
- Fixed a timezone edge case in delivery estimates: when the API reports only a calendar date, the estimate could be off by a day in time zones west of UTC — now stable everywhere.
|
|
172
|
-
|
|
173
173
|
[Older changelogs can be found there](CHANGELOG_OLD.md)
|
|
174
174
|
|
|
175
175
|
## Support
|
|
@@ -59,6 +59,16 @@ class StateManager {
|
|
|
59
59
|
* the same delivery keeps its bare id as long as it's unique.
|
|
60
60
|
*/
|
|
61
61
|
idOwner = /* @__PURE__ */ new Map();
|
|
62
|
+
/**
|
|
63
|
+
* L1: per-delivery-object memo for `parseStatus`. A poll parses the SAME
|
|
64
|
+
* delivery object at up to four sites (active filter, updateDelivery,
|
|
65
|
+
* updateSummary's isToday filter, combined window); memoizing keyed by the
|
|
66
|
+
* object means the parse — and its drift debug line — runs ONCE per delivery
|
|
67
|
+
* instead of per site. No reset needed: each poll's deliveries are fresh
|
|
68
|
+
* objects (JSON.parse) and GC'd afterwards, and nothing holds a long-lived
|
|
69
|
+
* delivery reference (idOwner/failedDeliveries/knownDeliveryIds store strings).
|
|
70
|
+
*/
|
|
71
|
+
statusMemo = /* @__PURE__ */ new WeakMap();
|
|
62
72
|
/**
|
|
63
73
|
* @param adapter The ioBroker adapter instance
|
|
64
74
|
*/
|
|
@@ -84,6 +94,22 @@ class StateManager {
|
|
|
84
94
|
* @param delivery The delivery to parse
|
|
85
95
|
*/
|
|
86
96
|
parseStatus(delivery) {
|
|
97
|
+
const memoized = this.statusMemo.get(delivery);
|
|
98
|
+
if (memoized !== void 0) {
|
|
99
|
+
return memoized;
|
|
100
|
+
}
|
|
101
|
+
const code = this.computeStatus(delivery);
|
|
102
|
+
this.statusMemo.set(delivery, code);
|
|
103
|
+
return code;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parse the status code without memoization. Split from {@link parseStatus}
|
|
107
|
+
* (L1) so the memo wraps a single pure computation — including the one-per-
|
|
108
|
+
* delivery drift log.
|
|
109
|
+
*
|
|
110
|
+
* @param delivery The delivery to parse
|
|
111
|
+
*/
|
|
112
|
+
computeStatus(delivery) {
|
|
87
113
|
const raw = delivery.status_code;
|
|
88
114
|
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
89
115
|
return Math.trunc(raw);
|
|
@@ -539,15 +565,17 @@ class StateManager {
|
|
|
539
565
|
* @param todayDeliveries Deliveries expected today
|
|
540
566
|
*/
|
|
541
567
|
calculateCombinedWindow(todayDeliveries) {
|
|
568
|
+
var _a;
|
|
542
569
|
const bounds = todayDeliveries.map((d) => this.windowBoundsMs(d, this.parseStatus(d))).filter((b) => b !== null);
|
|
543
570
|
if (bounds.length === 0) {
|
|
544
571
|
return "";
|
|
545
572
|
}
|
|
546
|
-
const minStart =
|
|
547
|
-
const maxEnd =
|
|
548
|
-
var
|
|
549
|
-
|
|
550
|
-
|
|
573
|
+
const minStart = bounds.reduce((m, b) => b.start < m ? b.start : m, bounds[0].start);
|
|
574
|
+
const maxEnd = bounds.reduce((m, b) => {
|
|
575
|
+
var _a2;
|
|
576
|
+
const e = (_a2 = b.end) != null ? _a2 : b.start;
|
|
577
|
+
return e > m ? e : m;
|
|
578
|
+
}, (_a = bounds[0].end) != null ? _a : bounds[0].start);
|
|
551
579
|
return StateManager.formatWindow(minStart, maxEnd);
|
|
552
580
|
}
|
|
553
581
|
/**
|
|
@@ -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, oneLine } from \"./coerce\";\nimport { packageName, statusLabel, tName, tText } from \"./i18n\";\nimport type { ParcelDelivery, ParcelEvent } from \"./types\";\nimport { UNKNOWN_STATUS_CODE } from \"./types\";\n\n/** Status codes that have expected delivery date/time: 2=In Transit, 4=Out for Delivery, 8=Info Received */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Upper bound for the `deliveries.*` object-view range query: the highest BMP\n * code unit, so the range covers every possible sanitized package id.\n */\nconst ID_RANGE_END = \"\uFFFF\";\n\n/** Max length of a sanitized package-id segment (collision suffix handles truncation clashes). */\nconst MAX_ID_LENGTH = 50;\n\n/**\n * v0.10.0 (I2): cap the parallel recursive deletes in cleanupDeliveries the\n * same way main.ts caps the update fan-out \u2014 a poll that suddenly loses many\n * packages must not flood the broker in one burst.\n */\nconst DELETE_BATCH_SIZE = 25;\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\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 * v0.10.0 (DP-5): package ids whose device object was ensured this process.\n * Replaces the former description+tracking signature map: with\n * `preserve: { common: [\"name\"] }` a rewrite never changed an existing\n * object's name anyway, so ensuring existence ONCE per process is the\n * honest version of what the signature cache actually did.\n */\n private readonly deviceEnsured = new Set<string>();\n\n /**\n * v0.7.2: package ids known to exist as device objects. Filled from the\n * object view ONCE after adapter start (reconciles leftovers from previous\n * runs), afterwards maintained in memory \u2014 `cleanupDeliveries` no longer\n * needs a DB round-trip per poll.\n */\n private knownDeliveryIds: Set<string> | null = null;\n\n /**\n * v0.4.2 (S3): which raw-tracking-key currently \"owns\" each sanitized id\n * within the running poll. Cleared via `resetPollState()` 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 * @param adapter The ioBroker adapter instance\n */\n constructor(adapter: AdapterInstance) {\n this.adapter = adapter;\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, MAX_ID_LENGTH) || \"unknown\"\n );\n }\n\n /**\n * Parse the status code from a delivery. The API sends an int; we also accept\n * a numeric string and fall back to the \"unknown\" sentinel (-1) for drift.\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n const raw = delivery.status_code;\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 if (Number.isFinite(n)) {\n return n;\n }\n }\n // API drift (non-numeric / non-string status_code). Return a visible\n // \"unknown\" sentinel instead of 0 (Delivered) \u2014 otherwise a garbage\n // status_code would silently filter the package out and remove it in\n // autoRemove mode. The active filter is `status !== 0`, so -1 stays visible.\n this.adapter.log.debug(\n `parseStatus drift: ${JSON.stringify(raw)} (type ${typeof raw}) \u2192 ${UNKNOWN_STATUS_CODE} (unknown, kept visible)`,\n );\n return UNKNOWN_STATUS_CODE;\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='${oneLine(owner)}' new='${oneLine(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): 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 * Extract the package-id segment from a relative object id\n * (`deliveries.<pkgId>` or `deliveries.<pkgId>.<state>`); \"\" when the id is\n * outside the deliveries tree. Single source for the id-schema knowledge\n * (v0.10.0, L15).\n *\n * @param relativeId Object id relative to the adapter namespace\n */\n private static pkgIdOf(relativeId: string): string {\n return relativeId.startsWith(\"deliveries.\") ? relativeId.slice(\"deliveries.\".length).split(\".\")[0] : \"\";\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 * @param pkgId Package id from the caller's deterministic pre-pass \u2014 always\n * computed via `packageId()` so the collision suffixing stays deterministic\n * (v0.10.0, L11: no test-only fallback path anymore).\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string, pkgId: string): Promise<void> {\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 // v0.10.0 (DP-5): ensure the device object once per process. `preserve:\n // name` keeps an existing name (user renames win), so the name \u2014 localized\n // fallback when the API sends no description (L18) \u2014 only matters at\n // first creation; the former per-change rewrite never had a visible effect.\n if (!this.deviceEnsured.has(pkgId)) {\n await this.adapter.extendObject(\n devicePath,\n {\n type: \"device\",\n common: {\n name: description || packageName(trackingNumber || pkgId),\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n this.deviceEnsured.add(pkgId);\n }\n this.knownDeliveryIds?.add(pkgId);\n\n const statusCode = this.parseStatus(delivery);\n let statusText = statusLabel(statusCode);\n if (statusText === undefined) {\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 label table is out of date.\n this.adapter.log.debug(`status code ${statusCode} has no status_* label, using fallback`);\n statusText = `Unknown (${statusCode})`;\n }\n\n const deliveryWindow = this.calculateDeliveryWindow(delivery, statusCode);\n const deliveryEstimate = this.calculateDeliveryEstimate(delivery, statusCode);\n const lastEvent = this.formatLastEvent(delivery);\n const lastLocation = this.extractLastLocation(delivery);\n\n // v0.10.0 (M5): ONE definition list drives the writes AND the lastUpdated\n // decision \u2014 the former parallel JSON.stringify signature array was a\n // silent drift trap (a new field added to one list but not the other).\n const stateDefs: [\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ][] = [\n [`${devicePath}.carrier`, tName(\"carrier\"), \"string\", \"text\", carrierName],\n [`${devicePath}.status`, tName(\"status\"), \"string\", \"text\", statusText],\n [`${devicePath}.statusCode`, tName(\"statusCode\"), \"number\", \"value\", statusCode],\n [`${devicePath}.description`, tName(\"description\"), \"string\", \"text\", description],\n [`${devicePath}.trackingNumber`, tName(\"trackingNumber\"), \"string\", \"text\", trackingNumber],\n [`${devicePath}.extraInfo`, tName(\"extraInfo\"), \"string\", \"text\", extraInfo],\n [`${devicePath}.deliveryWindow`, tName(\"deliveryWindow\"), \"string\", \"text\", deliveryWindow],\n [`${devicePath}.deliveryEstimate`, tName(\"deliveryEstimate\"), \"string\", \"text\", deliveryEstimate],\n [`${devicePath}.lastEvent`, tName(\"lastEvent\"), \"string\", \"text\", lastEvent],\n [`${devicePath}.lastLocation`, tName(\"lastLocation\"), \"string\", \"text\", lastLocation],\n ];\n const changed = await Promise.all(\n stateDefs.map(([id, name, type, role, val]) => this.createAndSet(id, name, type, role, val)),\n );\n\n // v0.10.0 (M5): `lastUpdated` = \"when the tracking data last CHANGED\".\n // The decision now rides on the broker's own setStateChanged answer\n // (notChanged=false \u21D2 a sibling value really differed in the DB), so an\n // adapter restart no longer stamps every package with a fresh timestamp \u2014\n // the old in-memory signature map always missed on the first poll after a\n // restart, and it was updated BEFORE its write survived (ASYNC-6).\n if (changed.some(Boolean)) {\n await this.createAndSet(\n `${devicePath}.lastUpdated`,\n 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(\"summary.activeCount\", tName(\"activeCount\"), \"number\", \"value\", activeDeliveries.length),\n this.createAndSet(\"summary.todayCount\", tName(\"todayCount\"), \"number\", \"value\", todayDeliveries.length),\n this.createAndSet(\n \"summary.deliveryWindow\",\n tName(\"summaryDeliveryWindow\"),\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer present in the API response.\n *\n * @param keepIds Package IDs the API still returns this poll (kept). Every\n * currently-known delivery NOT in this set is removed. The caller passes\n * ALL visible package ids, not only the ones whose state-write succeeded \u2014\n * a transient write failure must not delete a still-present package.\n */\n async cleanupDeliveries(keepIds: string[]): Promise<void> {\n // v0.7.2: the object view is queried only ONCE after adapter start to\n // reconcile leftovers from previous runs; afterwards the in-memory set\n // (maintained by updateDelivery + this prune) replaces the per-poll DB\n // round-trip.\n if (this.knownDeliveryIds === null) {\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.${ID_RANGE_END}`,\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 (and the known-set stays unseeded for the next poll).\n this.adapter.log.debug(\"cleanupDeliveries: no objects view available, skipping\");\n return;\n }\n this.knownDeliveryIds = new Set<string>();\n for (const row of objects.rows) {\n // The range query guarantees the namespace prefix \u2014 cut it instead of\n // pattern-replacing (v0.10.0, KISS-13).\n const pkgId = StateManager.pkgIdOf(row.id.slice(this.adapter.namespace.length + 1));\n if (pkgId) {\n this.knownDeliveryIds.add(pkgId);\n }\n }\n }\n\n const keepSet = new Set(keepIds);\n // v0.4.2 (S1): collect first, then delete in parallel \u2014 capped in batches\n // (v0.10.0, I2) like the update fan-out in main.ts.\n const toDelete = [...this.knownDeliveryIds].filter(pkgId => !keepSet.has(pkgId));\n const toDeleteSet = new Set(toDelete);\n\n for (let start = 0; start < toDelete.length; start += DELETE_BATCH_SIZE) {\n const batch = toDelete.slice(start, start + DELETE_BATCH_SIZE);\n await Promise.all(\n batch.map(async pkgId => {\n const relativeId = `deliveries.${pkgId}`;\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n this.deviceEnsured.delete(pkgId);\n }),\n );\n }\n\n // v0.9.0 (S2): prune createdIds for every removed package in ONE pass over\n // the set \u2014 O(created). A createdId is `deliveries.<pkgId>` or\n // `deliveries.<pkgId>.<state>` \u2014 extract the pkgId and drop it if removed.\n if (toDeleteSet.size > 0) {\n for (const id of [...this.createdIds]) {\n if (toDeleteSet.has(StateManager.pkgIdOf(id))) {\n this.createdIds.delete(id);\n }\n }\n }\n this.knownDeliveryIds = new Set(keepSet);\n }\n\n /**\n * Parse a parcel.app expected-date string to LOCAL epoch-millis.\n *\n * The API delivers `date_expected`/`date_expected_end` \"without specific\n * timezone information\"; parse with explicit local calendar components so the\n * value lands on the intended local day/time (`new Date(\"YYYY-MM-DD\")` would\n * be UTC midnight). `hasTime` is false for a bare date or a midnight time\n * (a day, not an hour-window). Ambiguous carrier formats (dotted, weekday\n * names) are deliberately NOT guessed \u2014 they return null rather than risk a\n * wrong date.\n *\n * @param value Raw date/time string from the API\n */\n private static parseExpectedToMs(value: unknown): { ms: number; hasTime: boolean } | null {\n if (typeof value !== \"string\") {\n return null;\n }\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?)?$/.exec(value.trim());\n if (!m) {\n return null;\n }\n const hasClock = m[4] !== undefined;\n const year = Number(m[1]);\n const month = Number(m[2]); // 1-12\n const day = Number(m[3]);\n const hour = hasClock ? Number(m[4]) : 0;\n const min = hasClock ? Number(m[5]) : 0;\n const sec = m[6] !== undefined ? Number(m[6]) : 0;\n // Range-validate the components. The regex only checks digit COUNT, not\n // value range, and `new Date(2026, 12, 40, 25, \u2026)` silently ROLLS OVER to a\n // wrong date (getTime() is NOT NaN). Reject out-of-range rather than guess.\n if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 || sec > 59) {\n return null;\n }\n const date = new Date(year, month - 1, day, hour, min, sec);\n // Catch day-of-month overflow the range check misses (Feb 30, Apr 31, \u2026):\n // a real date round-trips the month and day it was built from.\n if (Number.isNaN(date.getTime()) || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n const hasTime = hasClock && !(hour === 0 && min === 0 && sec === 0);\n return { ms: date.getTime(), hasTime };\n }\n\n /**\n * Resolve a delivery's expected window to epoch-millis bounds. Returns null\n * for non-trackable status or when there is no usable start time.\n *\n * Prefers the Unix timestamp fields; for carriers that report the window only\n * as a date/time string (`date_expected`/`date_expected_end`) it falls back to\n * those \u2014 but only when the string carries a real time-of-day (a bare date or\n * midnight is a day, not an hour-window). Carrier-agnostic.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private windowBoundsMs(delivery: ParcelDelivery, statusCode: number): { start: number; end: number | null } | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n const toMs = (timestamp: unknown): number | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const ms = ts * 1000;\n return Number.isNaN(new Date(ms).getTime()) ? null : ms;\n };\n const dateMs = (value: unknown): number | null => {\n const parsed = StateManager.parseExpectedToMs(value);\n return parsed && parsed.hasTime ? parsed.ms : null;\n };\n const start = toMs(delivery.timestamp_expected) ?? dateMs(delivery.date_expected);\n if (start === null) {\n return null;\n }\n const end = toMs(delivery.timestamp_expected_end) ?? dateMs(delivery.date_expected_end);\n return { start, end };\n }\n\n /**\n * Format epoch-millis as local HH:MM.\n *\n * @param ms Epoch milliseconds\n */\n private static formatHHMM(ms: number): string {\n const d = new Date(ms);\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n }\n\n /**\n * Local \"MM-DD HH:MM\" \u2014 used when a window spans more than one calendar day.\n *\n * @param ms Epoch milliseconds\n */\n private static formatDateHHMM(ms: number): string {\n const d = new Date(ms);\n const mm = (d.getMonth() + 1).toString().padStart(2, \"0\");\n const dd = d.getDate().toString().padStart(2, \"0\");\n return `${mm}-${dd} ${StateManager.formatHHMM(ms)}`;\n }\n\n /**\n * Whether two epoch-millis fall on the same LOCAL calendar day.\n *\n * @param aMs First epoch milliseconds\n * @param bMs Second epoch milliseconds\n */\n private static sameLocalDay(aMs: number, bMs: number): boolean {\n const a = new Date(aMs);\n const b = new Date(bMs);\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n }\n\n /**\n * Format a start\u2192end window as a local string. A real end (> start) on the\n * SAME day renders \"HH:MM - HH:MM\"; an end on a LATER day carries the date on\n * both sides (\"12-06 14:30 - 12-08 18:30\") so a multi-day window is not shown\n * as if it were same-day. No end, or an end <= start (reversed/equal), renders\n * just the start.\n *\n * @param startMs Window start (epoch ms)\n * @param endMs Window end (epoch ms) or null\n */\n private static formatWindow(startMs: number, endMs: number | null): string {\n if (endMs === null || endMs <= startMs) {\n return StateManager.formatHHMM(startMs);\n }\n return StateManager.sameLocalDay(startMs, endMs)\n ? `${StateManager.formatHHMM(startMs)} - ${StateManager.formatHHMM(endMs)}`\n : `${StateManager.formatDateHHMM(startMs)} - ${StateManager.formatDateHHMM(endMs)}`;\n }\n\n /**\n * Calculate a delivery time-window string from the resolved expected bounds.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(delivery: ParcelDelivery, statusCode: number): string {\n const bounds = this.windowBoundsMs(delivery, statusCode);\n if (!bounds) {\n return \"\";\n }\n return StateManager.formatWindow(bounds.start, bounds.end);\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 {\n // Shares the window's date parser (one source of format-truth). Only the\n // calendar day matters here, so the time-of-day flag is ignored; the\n // local-component parse keeps the day timezone-stable.\n const parsed = StateManager.parseExpectedToMs(delivery.date_expected);\n expectedDate = parsed ? new Date(parsed.ms) : null;\n }\n\n if (!expectedDate || Number.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 if (diffDays < 0) {\n return tText(\"estimateOverdue\");\n }\n if (diffDays === 0) {\n return tText(\"estimateToday\");\n }\n if (diffDays === 1) {\n return tText(\"estimateTomorrow\");\n }\n return tText(\"estimateDays\", 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 private getLatestEvent(delivery: ParcelDelivery): ParcelEvent | null {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return null;\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return null;\n }\n return latest;\n }\n\n private formatLastEvent(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\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 private extractLastLocation(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Combined delivery window for today's packages: earliest start to latest\n * end across all windows. Computed from the raw millis (not the formatted\n * strings) so the latest end always wins \u2014 fixes the earlier bug where the\n * end of the latest-*starting* window was used instead of the maximum end.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const bounds = todayDeliveries\n .map(d => this.windowBoundsMs(d, this.parseStatus(d)))\n .filter((b): b is { start: number; end: number | null } => b !== null);\n\n if (bounds.length === 0) {\n return \"\";\n }\n\n const minStart = Math.min(...bounds.map(b => b.start));\n const maxEnd = Math.max(...bounds.map(b => b.end ?? b.start));\n return StateManager.formatWindow(minStart, maxEnd);\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 * @returns true when the broker actually wrote the value (it differed or the\n * state was new) \u2014 the DB-backed \"did anything change\" signal driving\n * `lastUpdated` (v0.10.0, M5)\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<boolean> {\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 // The bundled @iobroker/types 7.1.2 types this promise as `string`, but\n // js-controller \u22657.2.2 (our dependency floor) resolves { id, notChanged }\n // \u2014 verified at v7.2.2: adapter.ts invokes the callback with\n // (null, res.id, res.notChanged) and tools.promisify(['id','notChanged'])\n // builds the object from exactly these named args. Narrow locally instead\n // of trusting the stale published type.\n const result: unknown = await this.adapter.setStateChangedAsync(id, { val, ack: true });\n // Only an explicit notChanged=false counts as a write \u2014 anything else\n // (missing field, drifted runtime) must not fake \"changed\" on every poll.\n return typeof result === \"object\" && result !== null && (result as { notChanged?: unknown }).notChanged === false;\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { type AdapterInstance } from \"@iobroker/adapter-core\";\nimport { coerceFiniteNumber, oneLine } from \"./coerce\";\nimport { packageName, statusLabel, tName, tText } from \"./i18n\";\nimport type { ParcelDelivery, ParcelEvent } from \"./types\";\nimport { UNKNOWN_STATUS_CODE } from \"./types\";\n\n/** Status codes that have expected delivery date/time: 2=In Transit, 4=Out for Delivery, 8=Info Received */\nconst TRACKABLE_STATUSES = new Set([2, 4, 8]);\n\n/**\n * Upper bound for the `deliveries.*` object-view range query: the highest BMP\n * code unit, so the range covers every possible sanitized package id.\n */\nconst ID_RANGE_END = \"\uFFFF\";\n\n/** Max length of a sanitized package-id segment (collision suffix handles truncation clashes). */\nconst MAX_ID_LENGTH = 50;\n\n/**\n * v0.10.0 (I2): cap the parallel recursive deletes in cleanupDeliveries the\n * same way main.ts caps the update fan-out \u2014 a poll that suddenly loses many\n * packages must not flood the broker in one burst.\n */\nconst DELETE_BATCH_SIZE = 25;\n\n/** Manages ioBroker states for parcel deliveries */\nexport class StateManager {\n private adapter: AdapterInstance;\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 * v0.10.0 (DP-5): package ids whose device object was ensured this process.\n * Replaces the former description+tracking signature map: with\n * `preserve: { common: [\"name\"] }` a rewrite never changed an existing\n * object's name anyway, so ensuring existence ONCE per process is the\n * honest version of what the signature cache actually did.\n */\n private readonly deviceEnsured = new Set<string>();\n\n /**\n * v0.7.2: package ids known to exist as device objects. Filled from the\n * object view ONCE after adapter start (reconciles leftovers from previous\n * runs), afterwards maintained in memory \u2014 `cleanupDeliveries` no longer\n * needs a DB round-trip per poll.\n */\n private knownDeliveryIds: Set<string> | null = null;\n\n /**\n * v0.4.2 (S3): which raw-tracking-key currently \"owns\" each sanitized id\n * within the running poll. Cleared via `resetPollState()` 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 * L1: per-delivery-object memo for `parseStatus`. A poll parses the SAME\n * delivery object at up to four sites (active filter, updateDelivery,\n * updateSummary's isToday filter, combined window); memoizing keyed by the\n * object means the parse \u2014 and its drift debug line \u2014 runs ONCE per delivery\n * instead of per site. No reset needed: each poll's deliveries are fresh\n * objects (JSON.parse) and GC'd afterwards, and nothing holds a long-lived\n * delivery reference (idOwner/failedDeliveries/knownDeliveryIds store strings).\n */\n private readonly statusMemo = new WeakMap<ParcelDelivery, number>();\n\n /**\n * @param adapter The ioBroker adapter instance\n */\n constructor(adapter: AdapterInstance) {\n this.adapter = adapter;\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, MAX_ID_LENGTH) || \"unknown\"\n );\n }\n\n /**\n * Parse the status code from a delivery. The API sends an int; we also accept\n * a numeric string and fall back to the \"unknown\" sentinel (-1) for drift.\n *\n * @param delivery The delivery to parse\n */\n parseStatus(delivery: ParcelDelivery): number {\n // L1: memoize per delivery object so the parse (and its drift log) runs\n // once per delivery per poll, not once per call site.\n const memoized = this.statusMemo.get(delivery);\n if (memoized !== undefined) {\n return memoized;\n }\n const code = this.computeStatus(delivery);\n this.statusMemo.set(delivery, code);\n return code;\n }\n\n /**\n * Parse the status code without memoization. Split from {@link parseStatus}\n * (L1) so the memo wraps a single pure computation \u2014 including the one-per-\n * delivery drift log.\n *\n * @param delivery The delivery to parse\n */\n private computeStatus(delivery: ParcelDelivery): number {\n const raw = delivery.status_code;\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 if (Number.isFinite(n)) {\n return n;\n }\n }\n // API drift (non-numeric / non-string status_code). Return a visible\n // \"unknown\" sentinel instead of 0 (Delivered) \u2014 otherwise a garbage\n // status_code would silently filter the package out and remove it in\n // autoRemove mode. The active filter is `status !== 0`, so -1 stays visible.\n this.adapter.log.debug(\n `parseStatus drift: ${JSON.stringify(raw)} (type ${typeof raw}) \u2192 ${UNKNOWN_STATUS_CODE} (unknown, kept visible)`,\n );\n return UNKNOWN_STATUS_CODE;\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='${oneLine(owner)}' new='${oneLine(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): 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 * Extract the package-id segment from a relative object id\n * (`deliveries.<pkgId>` or `deliveries.<pkgId>.<state>`); \"\" when the id is\n * outside the deliveries tree. Single source for the id-schema knowledge\n * (v0.10.0, L15).\n *\n * @param relativeId Object id relative to the adapter namespace\n */\n private static pkgIdOf(relativeId: string): string {\n return relativeId.startsWith(\"deliveries.\") ? relativeId.slice(\"deliveries.\".length).split(\".\")[0] : \"\";\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 * @param pkgId Package id from the caller's deterministic pre-pass \u2014 always\n * computed via `packageId()` so the collision suffixing stays deterministic\n * (v0.10.0, L11: no test-only fallback path anymore).\n */\n async updateDelivery(delivery: ParcelDelivery, carrierName: string, pkgId: string): Promise<void> {\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 // v0.10.0 (DP-5): ensure the device object once per process. `preserve:\n // name` keeps an existing name (user renames win), so the name \u2014 localized\n // fallback when the API sends no description (L18) \u2014 only matters at\n // first creation; the former per-change rewrite never had a visible effect.\n if (!this.deviceEnsured.has(pkgId)) {\n await this.adapter.extendObject(\n devicePath,\n {\n type: \"device\",\n common: {\n name: description || packageName(trackingNumber || pkgId),\n },\n native: {},\n },\n { preserve: { common: [\"name\"] } },\n );\n this.deviceEnsured.add(pkgId);\n }\n this.knownDeliveryIds?.add(pkgId);\n\n const statusCode = this.parseStatus(delivery);\n let statusText = statusLabel(statusCode);\n if (statusText === undefined) {\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 label table is out of date.\n this.adapter.log.debug(`status code ${statusCode} has no status_* label, using fallback`);\n statusText = `Unknown (${statusCode})`;\n }\n\n const deliveryWindow = this.calculateDeliveryWindow(delivery, statusCode);\n const deliveryEstimate = this.calculateDeliveryEstimate(delivery, statusCode);\n const lastEvent = this.formatLastEvent(delivery);\n const lastLocation = this.extractLastLocation(delivery);\n\n // v0.10.0 (M5): ONE definition list drives the writes AND the lastUpdated\n // decision \u2014 the former parallel JSON.stringify signature array was a\n // silent drift trap (a new field added to one list but not the other).\n const stateDefs: [\n id: string,\n name: ioBroker.StringOrTranslated,\n type: ioBroker.CommonType,\n role: string,\n val: ioBroker.StateValue,\n ][] = [\n [`${devicePath}.carrier`, tName(\"carrier\"), \"string\", \"text\", carrierName],\n [`${devicePath}.status`, tName(\"status\"), \"string\", \"text\", statusText],\n [`${devicePath}.statusCode`, tName(\"statusCode\"), \"number\", \"value\", statusCode],\n [`${devicePath}.description`, tName(\"description\"), \"string\", \"text\", description],\n [`${devicePath}.trackingNumber`, tName(\"trackingNumber\"), \"string\", \"text\", trackingNumber],\n [`${devicePath}.extraInfo`, tName(\"extraInfo\"), \"string\", \"text\", extraInfo],\n [`${devicePath}.deliveryWindow`, tName(\"deliveryWindow\"), \"string\", \"text\", deliveryWindow],\n [`${devicePath}.deliveryEstimate`, tName(\"deliveryEstimate\"), \"string\", \"text\", deliveryEstimate],\n [`${devicePath}.lastEvent`, tName(\"lastEvent\"), \"string\", \"text\", lastEvent],\n [`${devicePath}.lastLocation`, tName(\"lastLocation\"), \"string\", \"text\", lastLocation],\n ];\n const changed = await Promise.all(\n stateDefs.map(([id, name, type, role, val]) => this.createAndSet(id, name, type, role, val)),\n );\n\n // v0.10.0 (M5): `lastUpdated` = \"when the tracking data last CHANGED\".\n // The decision now rides on the broker's own setStateChanged answer\n // (notChanged=false \u21D2 a sibling value really differed in the DB), so an\n // adapter restart no longer stamps every package with a fresh timestamp \u2014\n // the old in-memory signature map always missed on the first poll after a\n // restart, and it was updated BEFORE its write survived (ASYNC-6).\n if (changed.some(Boolean)) {\n await this.createAndSet(\n `${devicePath}.lastUpdated`,\n 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(\"summary.activeCount\", tName(\"activeCount\"), \"number\", \"value\", activeDeliveries.length),\n this.createAndSet(\"summary.todayCount\", tName(\"todayCount\"), \"number\", \"value\", todayDeliveries.length),\n this.createAndSet(\n \"summary.deliveryWindow\",\n tName(\"summaryDeliveryWindow\"),\n \"string\",\n \"text\",\n this.calculateCombinedWindow(todayDeliveries),\n ),\n ]);\n }\n\n /**\n * Remove deliveries that are no longer present in the API response.\n *\n * @param keepIds Package IDs the API still returns this poll (kept). Every\n * currently-known delivery NOT in this set is removed. The caller passes\n * ALL visible package ids, not only the ones whose state-write succeeded \u2014\n * a transient write failure must not delete a still-present package.\n */\n async cleanupDeliveries(keepIds: string[]): Promise<void> {\n // v0.7.2: the object view is queried only ONCE after adapter start to\n // reconcile leftovers from previous runs; afterwards the in-memory set\n // (maintained by updateDelivery + this prune) replaces the per-poll DB\n // round-trip.\n if (this.knownDeliveryIds === null) {\n const objects = await this.adapter.getObjectViewAsync(\"system\", \"device\", {\n startkey: `${this.adapter.namespace}.deliveries.`,\n endkey: `${this.adapter.namespace}.deliveries.${ID_RANGE_END}`,\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 (and the known-set stays unseeded for the next poll).\n this.adapter.log.debug(\"cleanupDeliveries: no objects view available, skipping\");\n return;\n }\n this.knownDeliveryIds = new Set<string>();\n for (const row of objects.rows) {\n // The range query guarantees the namespace prefix \u2014 cut it instead of\n // pattern-replacing (v0.10.0, KISS-13).\n const pkgId = StateManager.pkgIdOf(row.id.slice(this.adapter.namespace.length + 1));\n if (pkgId) {\n this.knownDeliveryIds.add(pkgId);\n }\n }\n }\n\n const keepSet = new Set(keepIds);\n // v0.4.2 (S1): collect first, then delete in parallel \u2014 capped in batches\n // (v0.10.0, I2) like the update fan-out in main.ts.\n const toDelete = [...this.knownDeliveryIds].filter(pkgId => !keepSet.has(pkgId));\n const toDeleteSet = new Set(toDelete);\n\n for (let start = 0; start < toDelete.length; start += DELETE_BATCH_SIZE) {\n const batch = toDelete.slice(start, start + DELETE_BATCH_SIZE);\n await Promise.all(\n batch.map(async pkgId => {\n const relativeId = `deliveries.${pkgId}`;\n await this.adapter.delObjectAsync(relativeId, { recursive: true });\n this.adapter.log.debug(`Removed stale delivery: ${relativeId}`);\n this.deviceEnsured.delete(pkgId);\n }),\n );\n }\n\n // v0.9.0 (S2): prune createdIds for every removed package in ONE pass over\n // the set \u2014 O(created). A createdId is `deliveries.<pkgId>` or\n // `deliveries.<pkgId>.<state>` \u2014 extract the pkgId and drop it if removed.\n if (toDeleteSet.size > 0) {\n for (const id of [...this.createdIds]) {\n if (toDeleteSet.has(StateManager.pkgIdOf(id))) {\n this.createdIds.delete(id);\n }\n }\n }\n this.knownDeliveryIds = new Set(keepSet);\n }\n\n /**\n * Parse a parcel.app expected-date string to LOCAL epoch-millis.\n *\n * The API delivers `date_expected`/`date_expected_end` \"without specific\n * timezone information\"; parse with explicit local calendar components so the\n * value lands on the intended local day/time (`new Date(\"YYYY-MM-DD\")` would\n * be UTC midnight). `hasTime` is false for a bare date or a midnight time\n * (a day, not an hour-window). Ambiguous carrier formats (dotted, weekday\n * names) are deliberately NOT guessed \u2014 they return null rather than risk a\n * wrong date.\n *\n * @param value Raw date/time string from the API\n */\n private static parseExpectedToMs(value: unknown): { ms: number; hasTime: boolean } | null {\n if (typeof value !== \"string\") {\n return null;\n }\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[ T](\\d{2}):(\\d{2})(?::(\\d{2}))?)?$/.exec(value.trim());\n if (!m) {\n return null;\n }\n const hasClock = m[4] !== undefined;\n const year = Number(m[1]);\n const month = Number(m[2]); // 1-12\n const day = Number(m[3]);\n const hour = hasClock ? Number(m[4]) : 0;\n const min = hasClock ? Number(m[5]) : 0;\n const sec = m[6] !== undefined ? Number(m[6]) : 0;\n // Range-validate the components. The regex only checks digit COUNT, not\n // value range, and `new Date(2026, 12, 40, 25, \u2026)` silently ROLLS OVER to a\n // wrong date (getTime() is NOT NaN). Reject out-of-range rather than guess.\n if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || min > 59 || sec > 59) {\n return null;\n }\n const date = new Date(year, month - 1, day, hour, min, sec);\n // Catch day-of-month overflow the range check misses (Feb 30, Apr 31, \u2026):\n // a real date round-trips the month and day it was built from.\n if (Number.isNaN(date.getTime()) || date.getMonth() !== month - 1 || date.getDate() !== day) {\n return null;\n }\n const hasTime = hasClock && !(hour === 0 && min === 0 && sec === 0);\n return { ms: date.getTime(), hasTime };\n }\n\n /**\n * Resolve a delivery's expected window to epoch-millis bounds. Returns null\n * for non-trackable status or when there is no usable start time.\n *\n * Prefers the Unix timestamp fields; for carriers that report the window only\n * as a date/time string (`date_expected`/`date_expected_end`) it falls back to\n * those \u2014 but only when the string carries a real time-of-day (a bare date or\n * midnight is a day, not an hour-window). Carrier-agnostic.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private windowBoundsMs(delivery: ParcelDelivery, statusCode: number): { start: number; end: number | null } | null {\n if (!TRACKABLE_STATUSES.has(statusCode)) {\n return null;\n }\n const toMs = (timestamp: unknown): number | null => {\n const ts = coerceFiniteNumber(timestamp);\n if (ts === null || ts <= 0) {\n return null;\n }\n const ms = ts * 1000;\n return Number.isNaN(new Date(ms).getTime()) ? null : ms;\n };\n const dateMs = (value: unknown): number | null => {\n const parsed = StateManager.parseExpectedToMs(value);\n return parsed && parsed.hasTime ? parsed.ms : null;\n };\n const start = toMs(delivery.timestamp_expected) ?? dateMs(delivery.date_expected);\n if (start === null) {\n return null;\n }\n const end = toMs(delivery.timestamp_expected_end) ?? dateMs(delivery.date_expected_end);\n return { start, end };\n }\n\n /**\n * Format epoch-millis as local HH:MM.\n *\n * @param ms Epoch milliseconds\n */\n private static formatHHMM(ms: number): string {\n const d = new Date(ms);\n return `${d.getHours().toString().padStart(2, \"0\")}:${d.getMinutes().toString().padStart(2, \"0\")}`;\n }\n\n /**\n * Local \"MM-DD HH:MM\" \u2014 used when a window spans more than one calendar day.\n *\n * @param ms Epoch milliseconds\n */\n private static formatDateHHMM(ms: number): string {\n const d = new Date(ms);\n const mm = (d.getMonth() + 1).toString().padStart(2, \"0\");\n const dd = d.getDate().toString().padStart(2, \"0\");\n return `${mm}-${dd} ${StateManager.formatHHMM(ms)}`;\n }\n\n /**\n * Whether two epoch-millis fall on the same LOCAL calendar day.\n *\n * @param aMs First epoch milliseconds\n * @param bMs Second epoch milliseconds\n */\n private static sameLocalDay(aMs: number, bMs: number): boolean {\n const a = new Date(aMs);\n const b = new Date(bMs);\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n }\n\n /**\n * Format a start\u2192end window as a local string. A real end (> start) on the\n * SAME day renders \"HH:MM - HH:MM\"; an end on a LATER day carries the date on\n * both sides (\"12-06 14:30 - 12-08 18:30\") so a multi-day window is not shown\n * as if it were same-day. No end, or an end <= start (reversed/equal), renders\n * just the start.\n *\n * @param startMs Window start (epoch ms)\n * @param endMs Window end (epoch ms) or null\n */\n private static formatWindow(startMs: number, endMs: number | null): string {\n if (endMs === null || endMs <= startMs) {\n return StateManager.formatHHMM(startMs);\n }\n return StateManager.sameLocalDay(startMs, endMs)\n ? `${StateManager.formatHHMM(startMs)} - ${StateManager.formatHHMM(endMs)}`\n : `${StateManager.formatDateHHMM(startMs)} - ${StateManager.formatDateHHMM(endMs)}`;\n }\n\n /**\n * Calculate a delivery time-window string from the resolved expected bounds.\n *\n * @param delivery The delivery data\n * @param statusCode Pre-parsed status code\n */\n private calculateDeliveryWindow(delivery: ParcelDelivery, statusCode: number): string {\n const bounds = this.windowBoundsMs(delivery, statusCode);\n if (!bounds) {\n return \"\";\n }\n return StateManager.formatWindow(bounds.start, bounds.end);\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 {\n // Shares the window's date parser (one source of format-truth). Only the\n // calendar day matters here, so the time-of-day flag is ignored; the\n // local-component parse keeps the day timezone-stable.\n const parsed = StateManager.parseExpectedToMs(delivery.date_expected);\n expectedDate = parsed ? new Date(parsed.ms) : null;\n }\n\n if (!expectedDate || Number.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 if (diffDays < 0) {\n return tText(\"estimateOverdue\");\n }\n if (diffDays === 0) {\n return tText(\"estimateToday\");\n }\n if (diffDays === 1) {\n return tText(\"estimateTomorrow\");\n }\n return tText(\"estimateDays\", 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 private getLatestEvent(delivery: ParcelDelivery): ParcelEvent | null {\n if (!Array.isArray(delivery.events) || delivery.events.length === 0) {\n return null;\n }\n const latest = delivery.events[0];\n if (!latest || typeof latest !== \"object\") {\n return null;\n }\n return latest;\n }\n\n private formatLastEvent(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\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 private extractLastLocation(delivery: ParcelDelivery): string {\n const latest = this.getLatestEvent(delivery);\n if (!latest) {\n return \"\";\n }\n return typeof latest.location === \"string\" ? latest.location : \"\";\n }\n\n /**\n * Combined delivery window for today's packages: earliest start to latest\n * end across all windows. Computed from the raw millis (not the formatted\n * strings) so the latest end always wins \u2014 fixes the earlier bug where the\n * end of the latest-*starting* window was used instead of the maximum end.\n *\n * @param todayDeliveries Deliveries expected today\n */\n private calculateCombinedWindow(todayDeliveries: ParcelDelivery[]): string {\n const bounds = todayDeliveries\n .map(d => this.windowBoundsMs(d, this.parseStatus(d)))\n .filter((b): b is { start: number; end: number | null } => b !== null);\n\n if (bounds.length === 0) {\n return \"\";\n }\n\n // L3: fold instead of Math.min/max(...spread). The bounds array is capped\n // by the 1 MiB response limit, but a spread over a large array can still hit\n // V8's argument-count limit (RangeError); reduce is O(n) and unbounded-safe\n // \u2014 consistent with beszel's computeMaxTemp hardening.\n const minStart = bounds.reduce((m, b) => (b.start < m ? b.start : m), bounds[0].start);\n const maxEnd = bounds.reduce((m, b) => {\n const e = b.end ?? b.start;\n return e > m ? e : m;\n }, bounds[0].end ?? bounds[0].start);\n return StateManager.formatWindow(minStart, maxEnd);\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 * @returns true when the broker actually wrote the value (it differed or the\n * state was new) \u2014 the DB-backed \"did anything change\" signal driving\n * `lastUpdated` (v0.10.0, M5)\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<boolean> {\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 // The bundled @iobroker/types 7.1.2 types this promise as `string`, but\n // js-controller \u22657.2.2 (our dependency floor) resolves { id, notChanged }\n // \u2014 verified at v7.2.2: adapter.ts invokes the callback with\n // (null, res.id, res.notChanged) and tools.promisify(['id','notChanged'])\n // builds the object from exactly these named args. Narrow locally instead\n // of trusting the stale published type.\n const result: unknown = await this.adapter.setStateChangedAsync(id, { val, ack: true });\n // Only an explicit notChanged=false counts as a write \u2014 anything else\n // (missing field, drifted runtime) must not fake \"changed\" on every poll.\n return typeof result === \"object\" && result !== null && (result as { notChanged?: unknown }).notChanged === false;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["_a"]
|
|
7
7
|
}
|
package/build/main.js
CHANGED
|
@@ -91,6 +91,13 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
91
91
|
failedDeliveries = /* @__PURE__ */ new Set();
|
|
92
92
|
/** Timestamps of recent addDelivery POSTs — the S4 throttle window. */
|
|
93
93
|
addTimestamps = [];
|
|
94
|
+
/**
|
|
95
|
+
* L2: true while a checkConnection test GET is in flight. A test hits the same
|
|
96
|
+
* 20/hour GET budget as polling; this guards against a concurrent second test
|
|
97
|
+
* (double-click / admin re-render) stacking a redundant GET. A sequential
|
|
98
|
+
* re-test after the current one settles runs normally.
|
|
99
|
+
*/
|
|
100
|
+
testConnectionInFlight = false;
|
|
94
101
|
/**
|
|
95
102
|
* v0.4.4: short-lived test-clients spawned from `checkConnection` admin
|
|
96
103
|
* messages. The prod-`this.client` is what `onUnload` cancels, so these
|
|
@@ -225,6 +232,12 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
225
232
|
this.sendTo(obj.from, obj.command, { error: "API key is too short" }, obj.callback);
|
|
226
233
|
return;
|
|
227
234
|
}
|
|
235
|
+
if (this.testConnectionInFlight) {
|
|
236
|
+
this.log.debug("checkConnection: a test is already running");
|
|
237
|
+
this.sendTo(obj.from, obj.command, { error: "A connection test is already running \u2014 please wait" }, obj.callback);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.testConnectionInFlight = true;
|
|
228
241
|
const testClient = this.makeClient(key);
|
|
229
242
|
this.testClients.add(testClient);
|
|
230
243
|
try {
|
|
@@ -238,6 +251,7 @@ class ParcelappAdapter extends utils.Adapter {
|
|
|
238
251
|
);
|
|
239
252
|
} finally {
|
|
240
253
|
this.testClients.delete(testClient);
|
|
254
|
+
this.testConnectionInFlight = false;
|
|
241
255
|
}
|
|
242
256
|
}
|
|
243
257
|
/**
|
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 { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { coerceClampedInt, errText, isTrueish, oneLine } from \"./lib/coerce\";\nimport { ParcelClient, RETRY_AFTER_DEFAULT_SEC, RETRY_AFTER_MAX_SEC } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\nimport { DELIVERED_STATUS_CODE } from \"./lib/types\";\nimport type { AddDeliveryRequest } from \"./lib/types\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\n// Minimum 60s between polls. Also the natural pacing after addDelivery: a\n// typical single add still polls immediately (the last poll is >60s ago),\n// while a burst of adds collapses to at most one extra GET per minute.\nconst MIN_POLL_GAP_MS = 60_000;\n/** v0.4.2 (M6): minimum length for an apiKey value to even be considered valid. */\nconst MIN_API_KEY_LENGTH = 10;\n// v0.9.0 (S5): cap addDelivery field lengths. A sendTo caller is local, but a\n// runaway script must not push a multi-MB POST body to parcel.app. Identifiers\n// are short; this is generous for a description.\nconst MAX_ADD_FIELD_LEN = 512;\n// v0.9.0 (S3): cap the per-poll broker fan-out. Updates run in batches of this\n// size instead of all deliveries at once, so an abnormally large API response\n// can't flood the broker with thousands of concurrent writes.\nconst UPDATE_BATCH_SIZE = 25;\n// v0.9.0 (S4): client-side throttle on addDelivery POSTs. parcel.app enforces\n// ~20 POST/day server-side; this caps a runaway/buggy script's burst so it can't\n// hammer the API. The window is generous enough never to block a real batch-add\n// (the daily server limit is the real cap).\nconst MAX_ADDS_PER_WINDOW = 20;\nconst ADD_WINDOW_MS = 60_000;\n\n/**\n * Node transport-level error codes treated as (transient) NETWORK problems:\n * DNS, connection refused/reset, unreachable \u2014 plus EPIPE/ECONNABORTED/EPROTO\n * (v0.10.0, I9), which belong to the same warn+keep-retrying class.\n */\nconst NETWORK_ERROR_CODES = new Set([\n \"ENOTFOUND\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ENETUNREACH\",\n \"EHOSTUNREACH\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"ECONNABORTED\",\n \"EPROTO\",\n]);\n\n/**\n * v0.10.0 (L12): the seam contracts name exactly the members main uses \u2014\n * structural Picks let the orchestration tests inject plain object fakes\n * without unsafe double-casts, and the compiler documents what a fake must\n * provide.\n */\ntype ClientLike = Pick<\n ParcelClient,\n \"getDeliveries\" | \"getCarrierName\" | \"addDelivery\" | \"testConnection\" | \"cancelAll\"\n>;\ntype StateManagerLike = Pick<\n StateManager,\n \"resetPollState\" | \"packageId\" | \"parseStatus\" | \"updateDelivery\" | \"updateSummary\" | \"cleanupDeliveries\"\n>;\n\n/**\n * ioBroker adapter for parcel.app package tracking. Exported so the\n * orchestration unit tests can drive its lifecycle/poll handlers directly.\n */\nexport class ParcelappAdapter extends utils.Adapter {\n private client: ClientLike | null = null;\n private stateManager: StateManagerLike | null = null;\n /**\n * Factories for the HTTP client + state manager \u2014 default to the real\n * constructors. Test seams (fleet pattern): unit tests replace these with\n * fakes to exercise the poll orchestration (throttle/rate-limit interplay,\n * error routing, failure dedup) without real network.\n *\n * @param apiKey parcel.app API key\n */\n private makeClient: (apiKey: string) => ClientLike = apiKey =>\n new ParcelClient(apiKey, { debug: (m: string) => this.log.debug(m) });\n private makeStateManager: () => StateManagerLike = () => new StateManager(this);\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n /**\n * v0.10.0 (L2): set in onUnload. onReady checks it after its awaits so a\n * stop during the first poll can no longer arm the interval or log\n * \"started\" after the unload already ran; batch failures during shutdown\n * degrade to debug.\n */\n private unloaded = false;\n /**\n * Package ids (not raw tracking numbers) whose last updateDelivery failed.\n * Keyed like the states so the dedup survives a sanitize collision or a\n * missing tracking number; pruned each poll against the visible pkgIds.\n */\n private failedDeliveries = new Set<string>();\n /** Timestamps of recent addDelivery POSTs \u2014 the S4 throttle window. */\n private addTimestamps: number[] = [];\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<ClientLike>();\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n }\n\n private async onReady(): Promise<void> {\n try {\n // I18n.init resolves system.config.language itself (adapter-core reads\n // the foreign object internally; unknown languages fall back to English)\n // \u2014 the former separate getForeignObject round-trip and the local\n // resolveLanguage step are gone with the STATUS_LABELS table (L20).\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.log.debug(`onReady: starting (autoRemoveDelivered=${this.config.autoRemoveDelivered})`);\n\n await this.setState(\"info.connection\", { val: false, ack: true });\n\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 this.client = this.makeClient(apiKey.trim());\n this.stateManager = this.makeStateManager();\n\n try {\n await this.cleanupObsoleteStates();\n } catch (err) {\n // C8: a cleanup failure must not abort startup. Without this guard the\n // outer catch would skip arming the poll interval below, and the adapter\n // would never poll until a manual restart. Degrade to a warning.\n this.log.warn(`cleanupObsoleteStates failed (continuing): ${errText(err)}`);\n }\n\n await this.poll();\n\n // v0.10.0 (L2): a stop during the awaits above must not arm a timer or\n // log a start line after onUnload already ran.\n if (this.unloaded) {\n return;\n }\n\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\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(() => {\n void this.poll().catch(err => this.log.error(`Scheduled poll failed: ${errText(err)}`));\n }, intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n // v0.10.0 (L4): a transient startup failure (i18n files, DB hiccup) used\n // to leave a green-looking zombie \u2014 no client, no timer, no retry until\n // a manual restart. Terminate instead: js-controller restarts the\n // instance, which self-heals the transient case; its restart-loop guard\n // backstops a persistent one.\n if (!this.unloaded) {\n this.terminate(\"startup failed \u2014 requesting restart\", utils.EXIT_CODES.START_IMMEDIATELY_AFTER_STOP);\n }\n }\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 this.unloaded = true;\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. cancelAll also marks the\n // client terminal (v0.10.0, L3) \u2014 requests started after this point\n // are refused instead of opening fresh connections.\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 // 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 try {\n this.log.debug(`onUnload error (ignored): ${errText(err)}`);\n } catch {\n /* logger already gone \u2014 nothing left to report to */\n }\n } finally {\n // v0.10.0 (I7): the callback is the contract with js-controller \u2014\n // structurally guaranteed exactly once, even if the catch-path throws.\n callback();\n }\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n // v0.4.3 (F1): entry log BEFORE the early-return \u2014 broadcast messages\n // without callback wouldn't be visible otherwise. Inside the try since\n // v0.10.0 (I6) so the handler body is gap-free top-level guarded.\n this.log.debug(\n `onMessage: command='${oneLine(String(obj?.command ?? \"\"))}' from='${obj?.from}' has-callback=${!!obj?.callback}`,\n );\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n switch (obj.command) {\n case \"checkConnection\":\n await this.handleCheckConnection(obj);\n break;\n case \"addDelivery\":\n await this.handleAddDelivery(obj);\n break;\n default:\n // v0.4.3 (F6): trace unknown command before sendTo.\n this.log.debug(`onMessage: unknown command '${oneLine(String(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. The\n // reply itself is guarded too (v0.10.0, I6) \u2014 a synchronous sendTo\n // throw must not escape the message handler. checkConnection replies\n // use the admin {error} envelope (H1), scripts keep the documented\n // {success, error_message} shape.\n try {\n this.log.debug(`onMessage: '${oneLine(String(obj?.command ?? \"\"))}' failed: ${errText(err)}`);\n if (obj?.callback) {\n const reply =\n obj.command === \"checkConnection\"\n ? { error: errText(err) }\n : { success: false, error_message: errText(err) };\n this.sendTo(obj.from, obj.command, reply, obj.callback);\n }\n } catch {\n /* reply channel gone \u2014 nothing left to do */\n }\n }\n }\n\n /**\n * Admin \"Test Connection\" button (H1). The jsonConfig sendTo component reads\n * ONLY `response.error` / `response.result` \u2014 never success/message \u2014 so the\n * internal `{success, message}` result is mapped to that contract here.\n * Before this, a FAILED test showed a false-positive \"Ok\" in the admin\n * (fleet fix; beszel's message-router is the model).\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleCheckConnection(obj: ioBroker.Message): Promise<void> {\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, { error: \"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 (via the makeClient seam).\n const testClient = this.makeClient(key);\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(\n obj.from,\n obj.command,\n result.success ? { result: result.message } : { error: result.message },\n obj.callback,\n );\n } finally {\n this.testClients.delete(testClient);\n }\n }\n\n /**\n * Reply an addDelivery failure to the sendTo caller. This is the documented\n * script API envelope (`{success: false, error_message}`) \u2014 unchanged for\n * backward compatibility; only the admin checkConnection uses {error}.\n *\n * @param obj The sendTo message being answered\n * @param message Human-readable failure reason\n */\n private replyAddError(obj: ioBroker.Message, message: string): void {\n this.sendTo(obj.from, obj.command, { success: false, error_message: message }, obj.callback);\n }\n\n /**\n * Script-facing addDelivery command: validate the message shape, cap field\n * lengths, throttle bursts, forward to the API and trigger a poll on\n * success. Extracted from the onMessage switch (M9) \u2014 one command, one\n * method, one change reason.\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleAddDelivery(obj: ioBroker.Message): Promise<void> {\n if (!this.client) {\n // v0.4.3 (F4): trace addDelivery-before-init.\n this.log.debug(\"addDelivery: adapter not initialized\");\n this.replyAddError(obj, \"Adapter not initialized\");\n return;\n }\n // v0.7.2: obj.message is `unknown`-shaped \u2014 a script calling\n // sendTo(\"parcelapp\", \"addDelivery\", null) used to surface as an\n // ugly TypeError through the catch instead of a clear validation\n // message. Coerce to a plain object and validate required fields.\n const raw = obj.message;\n const msg = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};\n if (\n typeof msg.tracking_number !== \"string\" ||\n msg.tracking_number.length === 0 ||\n typeof msg.carrier_code !== \"string\" ||\n msg.carrier_code.length === 0 ||\n typeof msg.description !== \"string\" ||\n msg.description.length === 0\n ) {\n this.log.debug(\"addDelivery: missing tracking_number/carrier_code/description in message\");\n this.replyAddError(obj, \"tracking_number, carrier_code and description are required\");\n return;\n }\n // v0.9.0 (S5): cap field lengths so a runaway local script can't push\n // a multi-MB POST body to parcel.app. Checked after the required-field\n // guard above so the two validation messages stay distinct.\n if (\n msg.tracking_number.length > MAX_ADD_FIELD_LEN ||\n msg.carrier_code.length > MAX_ADD_FIELD_LEN ||\n msg.description.length > MAX_ADD_FIELD_LEN ||\n (typeof msg.language === \"string\" && msg.language.length > MAX_ADD_FIELD_LEN)\n ) {\n this.log.debug(\"addDelivery: a field exceeds the maximum length\");\n this.replyAddError(obj, `each field must be at most ${MAX_ADD_FIELD_LEN} characters`);\n return;\n }\n // Pass the optional API fields through when the caller supplies them\n // (language: ISO 639-1 two-letter code; send_push_confirmation: push\n // notification once the delivery is added).\n const request: AddDeliveryRequest = {\n tracking_number: msg.tracking_number,\n carrier_code: msg.carrier_code,\n description: msg.description,\n };\n if (typeof msg.language === \"string\" && msg.language.length > 0) {\n request.language = msg.language;\n }\n if (typeof msg.send_push_confirmation === \"boolean\") {\n request.send_push_confirmation = msg.send_push_confirmation;\n }\n // v0.9.0 (S4): throttle addDelivery POSTs. parcel.app caps ~20/day\n // server-side; this stops a runaway/buggy script from hammering the API\n // with a burst. Record the attempt before the await so concurrent\n // callers count too.\n const nowMs = Date.now();\n this.addTimestamps = this.addTimestamps.filter(t => nowMs - t < ADD_WINDOW_MS);\n if (this.addTimestamps.length >= MAX_ADDS_PER_WINDOW) {\n this.log.warn(`addDelivery throttled: more than ${MAX_ADDS_PER_WINDOW} requests within ${ADD_WINDOW_MS / 1000}s`);\n this.replyAddError(obj, `too many addDelivery requests; max ${MAX_ADDS_PER_WINDOW} per ${ADD_WINDOW_MS / 1000}s`);\n return;\n }\n this.addTimestamps.push(nowMs);\n const addResult = await this.client.addDelivery(request);\n // v0.4.3 (F5): trace addDelivery result with the (flattened) tracking number.\n // v0.10.0 (L9): the drift-guarded isTrueish \u2014 getDeliveries hardens the same\n // API flag; a drifted `success: \"false\"` string must not read as truthy.\n const added = isTrueish(addResult.success);\n this.log.debug(`addDelivery: '${oneLine(request.tracking_number)}' result=${added ? \"ok\" : \"fail\"}`);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (added) {\n // v0.10.0 (L5): plain poll, no force \u2014 a single add still polls right\n // away (the last poll is usually >60s back), but an add-burst can no\n // longer stack force-GETs past the 20/h API budget. Nothing is lost:\n // the server caches the list ~45-90 min, a fresh package rarely shows\n // tracking data immediately anyway.\n void this.poll().catch(err => this.log.error(`Poll after addDelivery failed: ${errText(err)}`));\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n // One getObject per adapter start, forever \u2014 deliberately kept (I3/ARCH-24):\n // a one-shot migration marker would cost more mechanics than this read.\n // Drop the list entirely at the next major once 0.1.x installs are gone.\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 * v0.10.0 (M1): the client codes every failure it raises (TIMEOUT,\n * PARSE_ERROR, ABORTED, RATE_LIMITED, \u2026) \u2014 a present machine code always\n * wins. The message-substring sniff only remains for code-less foreign\n * errors, so an API error_message merely CONTAINING \"timeout\" can no longer\n * be misclassified.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code) {\n if (NETWORK_ERROR_CODES.has(error.code)) {\n return \"NETWORK\";\n }\n if (error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code;\n }\n if (error.message.includes(\"timeout\")) {\n return \"TIMEOUT\";\n }\n return \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n // v0.10.0 (M4): make the re-entry/uninitialized skip visible like the\n // rate-limit/throttle skips below \u2014 this used to be the one silent spot\n // where \"the adapter does nothing\" left no trace.\n this.log.debug(\"Skipping poll \u2014 already running or not initialized\");\n return;\n }\n // v0.10.0 (L14): local bindings instead of non-null assertions in the\n // closures below \u2014 provable narrowing the compiler checks, robust against\n // a future refactor nulling the fields mid-poll.\n const client = this.client;\n const stateManager = this.stateManager;\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 (also paces the poll after a\n // successful addDelivery \u2014 see handleAddDelivery).\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 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.setStateChangedAsync(\"info.connection\", { val: true, ack: true });\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => stateManager.parseStatus(d) !== DELIVERED_STATUS_CODE);\n const visibleDeliveries = autoRemoveMode ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset the per-poll collision tracker, then compute every\n // package id in a deterministic sequential pre-pass (stable array order)\n // BEFORE the parallel updates \u2014 collision-suffixing is then deterministic\n // and packageId runs exactly once per delivery instead of twice.\n stateManager.resetPollState();\n const pkgIds = visibleDeliveries.map(d => stateManager.packageId(d));\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 // v0.9.0 (S3): process the updates in bounded batches instead of one\n // broker fan-out for ALL deliveries at once. The keep-set is still every\n // visible pkgId (computed above), so this only caps concurrency \u2014 it never\n // drops a package. A normal poll (a handful of packages) is a single batch.\n if (visibleDeliveries.length > UPDATE_BATCH_SIZE) {\n this.log.debug(`Updating ${visibleDeliveries.length} deliveries in batches of ${UPDATE_BATCH_SIZE}`);\n }\n for (let start = 0; start < visibleDeliveries.length; start += UPDATE_BATCH_SIZE) {\n const batch = visibleDeliveries.slice(start, start + UPDATE_BATCH_SIZE);\n await Promise.all(\n batch.map(async (delivery, offset) => {\n const pkgId = pkgIds[start + offset];\n // Pre-sanitize the externally-sourced strings once for logging. The\n // fields are optional (the API can drop them), so default to \"\" before\n // oneLine flattens them onto a single log line.\n const tracking = oneLine(delivery.tracking_number ?? \"\");\n const carrier = oneLine(delivery.carrier_code ?? \"\");\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 // v0.10.0 (M6): status_code is typed number|string (drift), so it\n // is flattened like the other externally-sourced fields.\n this.log.debug(\n `updateDelivery: '${tracking}' carrier=${carrier} status=${oneLine(String(delivery.status_code))}`,\n );\n const carrierName = await client.getCarrierName(delivery.carrier_code);\n await stateManager.updateDelivery(delivery, carrierName, pkgId);\n this.failedDeliveries.delete(pkgId);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(pkgId)) {\n this.log.debug(`Failed to update '${tracking}': ${msg}`);\n } else if (this.unloaded) {\n // v0.10.0 (L2): broker teardown mid-batch is expected noise\n // during shutdown, not a per-package warning.\n this.log.debug(`Failed to update '${tracking}' during shutdown: ${msg}`);\n } else {\n this.log.warn(`Failed to update '${tracking}': ${msg}`);\n this.failedDeliveries.add(pkgId);\n }\n }\n }),\n );\n }\n\n // v0.9.0 (C1): keep-set = EVERY package the API still returns this poll\n // (pkgIds), NOT only the writes that just succeeded. A transient\n // updateDelivery failure leaves a package's states stale, but it must not\n // drop the package from cleanup \u2014 that would delete a still-present\n // package's states (and any user-set device name) at green info.connection.\n // v0.10.0 (M2): broker-side failures in cleanup/summary are NOT API\n // failures \u2014 they must neither flip info.connection to false (the GET\n // above just succeeded) nor run through the API error classification.\n try {\n await stateManager.cleanupDeliveries(pkgIds);\n // Update summary (always uses active/non-delivered)\n await stateManager.updateSummary(activeDeliveries);\n } catch (err) {\n this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${errText(err)}`);\n }\n\n // Keep failedDeliveries bounded: drop entries for package ids no longer\n // present, so packages that vanish from the API don't linger forever.\n const seenPkgIds = new Set(pkgIds);\n for (const id of [...this.failedDeliveries]) {\n if (!seenPkgIds.has(id)) {\n this.failedDeliveries.delete(id);\n }\n }\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n await this.handlePollError(err as Error & { code?: string; retryAfterSeconds?: number });\n } finally {\n this.isPolling = false;\n }\n }\n\n /**\n * Classify + route a poll failure: log level, dedup, cooldown and the\n * info.connection=false write. Extracted from poll()'s catch (M9) so the\n * happy path reads as a plain sequence and the error policy sits next to\n * classifyError. Dispatches on the CLASSIFIED code only (L6) \u2014 one source\n * of truth for the error class.\n *\n * @param error The poll failure (usually an ApiError from the client)\n */\n private async handlePollError(error: Error & { code?: string; retryAfterSeconds?: number }): Promise<void> {\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n switch (errorCode) {\n case \"ABORTED\":\n // v0.10.0 (M1): expected during shutdown \u2014 cancelAll aborts the\n // in-flight GET. A deliberate stop must not paint a red error line.\n this.log.debug(`Poll aborted: ${error.message}`);\n break;\n case \"RATE_LIMITED\": {\n // v0.4.2 (M9): clamp Retry-After into [60s, 24h] (shared constants\n // with the client parser, L7). A bogus 0/negative/fractional value\n // must neither wipe the cooldown nor set it for milliseconds.\n const rawCooldown = error.retryAfterSeconds ?? 0;\n const cooldownSec =\n Number.isFinite(rawCooldown) && rawCooldown > 0\n ? Math.min(RETRY_AFTER_MAX_SEC, Math.max(60, Math.floor(rawCooldown)))\n : RETRY_AFTER_DEFAULT_SEC;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n // v0.10.0 (M3): warn once \u2014 a persistent 429 repeats at debug.\n const line = `Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`;\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.warn(line);\n }\n break;\n }\n case \"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 // v0.10.0 (M3): once at error level, repeats at debug \u2014 not 144\n // identical error lines per day for one unchanged account problem.\n const line =\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 if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"INVALID_API_KEY\": {\n // v0.10.0 (M3): first occurrence at error (the user must fix the\n // config; info.connection goes red too) \u2014 repeats at debug.\n const line = \"Invalid API key \u2014 please check your parcel.app API key\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"NETWORK\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n }\n break;\n case \"TIMEOUT\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n }\n break;\n default:\n 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 {\n this.log.error(`Poll failed: ${error.message}`);\n }\n }\n\n // C2: setStateChangedAsync avoids redundant `false` writes on sustained\n // failure. The `.catch` keeps poll() from rejecting when the broker is\n // already down, so the fire-and-forget callers never see an unhandled\n // rejection (no global process handler needed).\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker shutting down \u2014 ignore */\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": "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",
|
|
4
|
+
"sourcesContent": ["import * as utils from \"@iobroker/adapter-core\";\nimport { I18n } from \"@iobroker/adapter-core\";\nimport { join } from \"node:path\";\nimport { coerceClampedInt, errText, isTrueish, oneLine } from \"./lib/coerce\";\nimport { ParcelClient, RETRY_AFTER_DEFAULT_SEC, RETRY_AFTER_MAX_SEC } from \"./lib/parcel-client\";\nimport { StateManager } from \"./lib/state-manager\";\nimport { DELIVERED_STATUS_CODE } from \"./lib/types\";\nimport type { AddDeliveryRequest } from \"./lib/types\";\n\nconst MIN_POLL_INTERVAL = 5;\nconst MAX_POLL_INTERVAL = 60;\nconst DEFAULT_POLL_INTERVAL = 10;\n// Minimum 60s between polls. Also the natural pacing after addDelivery: a\n// typical single add still polls immediately (the last poll is >60s ago),\n// while a burst of adds collapses to at most one extra GET per minute.\nconst MIN_POLL_GAP_MS = 60_000;\n/** v0.4.2 (M6): minimum length for an apiKey value to even be considered valid. */\nconst MIN_API_KEY_LENGTH = 10;\n// v0.9.0 (S5): cap addDelivery field lengths. A sendTo caller is local, but a\n// runaway script must not push a multi-MB POST body to parcel.app. Identifiers\n// are short; this is generous for a description.\nconst MAX_ADD_FIELD_LEN = 512;\n// v0.9.0 (S3): cap the per-poll broker fan-out. Updates run in batches of this\n// size instead of all deliveries at once, so an abnormally large API response\n// can't flood the broker with thousands of concurrent writes.\nconst UPDATE_BATCH_SIZE = 25;\n// v0.9.0 (S4): client-side throttle on addDelivery POSTs. parcel.app enforces\n// ~20 POST/day server-side; this caps a runaway/buggy script's burst so it can't\n// hammer the API. The window is generous enough never to block a real batch-add\n// (the daily server limit is the real cap).\nconst MAX_ADDS_PER_WINDOW = 20;\nconst ADD_WINDOW_MS = 60_000;\n\n/**\n * Node transport-level error codes treated as (transient) NETWORK problems:\n * DNS, connection refused/reset, unreachable \u2014 plus EPIPE/ECONNABORTED/EPROTO\n * (v0.10.0, I9), which belong to the same warn+keep-retrying class.\n */\nconst NETWORK_ERROR_CODES = new Set([\n \"ENOTFOUND\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ENETUNREACH\",\n \"EHOSTUNREACH\",\n \"EAI_AGAIN\",\n \"EPIPE\",\n \"ECONNABORTED\",\n \"EPROTO\",\n]);\n\n/**\n * v0.10.0 (L12): the seam contracts name exactly the members main uses \u2014\n * structural Picks let the orchestration tests inject plain object fakes\n * without unsafe double-casts, and the compiler documents what a fake must\n * provide.\n */\ntype ClientLike = Pick<\n ParcelClient,\n \"getDeliveries\" | \"getCarrierName\" | \"addDelivery\" | \"testConnection\" | \"cancelAll\"\n>;\ntype StateManagerLike = Pick<\n StateManager,\n \"resetPollState\" | \"packageId\" | \"parseStatus\" | \"updateDelivery\" | \"updateSummary\" | \"cleanupDeliveries\"\n>;\n\n/**\n * ioBroker adapter for parcel.app package tracking. Exported so the\n * orchestration unit tests can drive its lifecycle/poll handlers directly.\n */\nexport class ParcelappAdapter extends utils.Adapter {\n private client: ClientLike | null = null;\n private stateManager: StateManagerLike | null = null;\n /**\n * Factories for the HTTP client + state manager \u2014 default to the real\n * constructors. Test seams (fleet pattern): unit tests replace these with\n * fakes to exercise the poll orchestration (throttle/rate-limit interplay,\n * error routing, failure dedup) without real network.\n *\n * @param apiKey parcel.app API key\n */\n private makeClient: (apiKey: string) => ClientLike = apiKey =>\n new ParcelClient(apiKey, { debug: (m: string) => this.log.debug(m) });\n private makeStateManager: () => StateManagerLike = () => new StateManager(this);\n private pollTimer: ioBroker.Interval | undefined = undefined;\n private isPolling = false;\n private lastPollTime = 0;\n private rateLimitedUntil = 0;\n private lastErrorCode = \"\";\n /**\n * v0.10.0 (L2): set in onUnload. onReady checks it after its awaits so a\n * stop during the first poll can no longer arm the interval or log\n * \"started\" after the unload already ran; batch failures during shutdown\n * degrade to debug.\n */\n private unloaded = false;\n /**\n * Package ids (not raw tracking numbers) whose last updateDelivery failed.\n * Keyed like the states so the dedup survives a sanitize collision or a\n * missing tracking number; pruned each poll against the visible pkgIds.\n */\n private failedDeliveries = new Set<string>();\n /** Timestamps of recent addDelivery POSTs \u2014 the S4 throttle window. */\n private addTimestamps: number[] = [];\n /**\n * L2: true while a checkConnection test GET is in flight. A test hits the same\n * 20/hour GET budget as polling; this guards against a concurrent second test\n * (double-click / admin re-render) stacking a redundant GET. A sequential\n * re-test after the current one settles runs normally.\n */\n private testConnectionInFlight = false;\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<ClientLike>();\n\n /** @param options Adapter options */\n public constructor(options: Partial<utils.AdapterOptions> = {}) {\n super({\n ...options,\n name: \"parcelapp\",\n });\n this.on(\"ready\", this.onReady.bind(this));\n this.on(\"unload\", this.onUnload.bind(this));\n this.on(\"message\", this.onMessage.bind(this));\n }\n\n private async onReady(): Promise<void> {\n try {\n // I18n.init resolves system.config.language itself (adapter-core reads\n // the foreign object internally; unknown languages fall back to English)\n // \u2014 the former separate getForeignObject round-trip and the local\n // resolveLanguage step are gone with the STATUS_LABELS table (L20).\n await I18n.init(join(this.adapterDir, \"admin\"), this);\n this.log.debug(`onReady: starting (autoRemoveDelivered=${this.config.autoRemoveDelivered})`);\n\n await this.setState(\"info.connection\", { val: false, ack: true });\n\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 this.client = this.makeClient(apiKey.trim());\n this.stateManager = this.makeStateManager();\n\n try {\n await this.cleanupObsoleteStates();\n } catch (err) {\n // C8: a cleanup failure must not abort startup. Without this guard the\n // outer catch would skip arming the poll interval below, and the adapter\n // would never poll until a manual restart. Degrade to a warning.\n this.log.warn(`cleanupObsoleteStates failed (continuing): ${errText(err)}`);\n }\n\n await this.poll();\n\n // v0.10.0 (L2): a stop during the awaits above must not arm a timer or\n // log a start line after onUnload already ran.\n if (this.unloaded) {\n return;\n }\n\n const interval = ParcelappAdapter.coercePollInterval(this.config.pollInterval);\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(() => {\n void this.poll().catch(err => this.log.error(`Scheduled poll failed: ${errText(err)}`));\n }, intervalMs);\n\n this.log.info(`Parcel tracking started \u2014 polling every ${interval} minutes`);\n } catch (err: unknown) {\n this.log.error(`onReady failed: ${errText(err)}`);\n // v0.10.0 (L4): a transient startup failure (i18n files, DB hiccup) used\n // to leave a green-looking zombie \u2014 no client, no timer, no retry until\n // a manual restart. Terminate instead: js-controller restarts the\n // instance, which self-heals the transient case; its restart-loop guard\n // backstops a persistent one.\n if (!this.unloaded) {\n this.terminate(\"startup failed \u2014 requesting restart\", utils.EXIT_CODES.START_IMMEDIATELY_AFTER_STOP);\n }\n }\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 this.unloaded = true;\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. cancelAll also marks the\n // client terminal (v0.10.0, L3) \u2014 requests started after this point\n // are refused instead of opening fresh connections.\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 // 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 try {\n this.log.debug(`onUnload error (ignored): ${errText(err)}`);\n } catch {\n /* logger already gone \u2014 nothing left to report to */\n }\n } finally {\n // v0.10.0 (I7): the callback is the contract with js-controller \u2014\n // structurally guaranteed exactly once, even if the catch-path throws.\n callback();\n }\n }\n\n private async onMessage(obj: ioBroker.Message): Promise<void> {\n try {\n // v0.4.3 (F1): entry log BEFORE the early-return \u2014 broadcast messages\n // without callback wouldn't be visible otherwise. Inside the try since\n // v0.10.0 (I6) so the handler body is gap-free top-level guarded.\n this.log.debug(\n `onMessage: command='${oneLine(String(obj?.command ?? \"\"))}' from='${obj?.from}' has-callback=${!!obj?.callback}`,\n );\n if (!obj?.command || !obj.callback) {\n return;\n }\n\n switch (obj.command) {\n case \"checkConnection\":\n await this.handleCheckConnection(obj);\n break;\n case \"addDelivery\":\n await this.handleAddDelivery(obj);\n break;\n default:\n // v0.4.3 (F6): trace unknown command before sendTo.\n this.log.debug(`onMessage: unknown command '${oneLine(String(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. The\n // reply itself is guarded too (v0.10.0, I6) \u2014 a synchronous sendTo\n // throw must not escape the message handler. checkConnection replies\n // use the admin {error} envelope (H1), scripts keep the documented\n // {success, error_message} shape.\n try {\n this.log.debug(`onMessage: '${oneLine(String(obj?.command ?? \"\"))}' failed: ${errText(err)}`);\n if (obj?.callback) {\n const reply =\n obj.command === \"checkConnection\"\n ? { error: errText(err) }\n : { success: false, error_message: errText(err) };\n this.sendTo(obj.from, obj.command, reply, obj.callback);\n }\n } catch {\n /* reply channel gone \u2014 nothing left to do */\n }\n }\n }\n\n /**\n * Admin \"Test Connection\" button (H1). The jsonConfig sendTo component reads\n * ONLY `response.error` / `response.result` \u2014 never success/message \u2014 so the\n * internal `{success, message}` result is mapped to that contract here.\n * Before this, a FAILED test showed a false-positive \"Ok\" in the admin\n * (fleet fix; beszel's message-router is the model).\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleCheckConnection(obj: ioBroker.Message): Promise<void> {\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, { error: \"API key is too short\" }, obj.callback);\n return;\n }\n // L2: a Test-Connection GET counts against the same 20/hour budget as\n // polling. Guard against a concurrent second test (double-click / admin\n // re-render) so stacked clicks can't each burn a GET (which could later trip\n // the poll's rate-limit cooldown). Set BEFORE the await so the check is\n // synchronous against a still-in-flight first test.\n if (this.testConnectionInFlight) {\n this.log.debug(\"checkConnection: a test is already running\");\n this.sendTo(obj.from, obj.command, { error: \"A connection test is already running \u2014 please wait\" }, obj.callback);\n return;\n }\n this.testConnectionInFlight = true;\n // v0.4.3: same debug-logger as the prod client so checkConnection\n // failures get the same HTTPS-layer trace (via the makeClient seam).\n const testClient = this.makeClient(key);\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(\n obj.from,\n obj.command,\n result.success ? { result: result.message } : { error: result.message },\n obj.callback,\n );\n } finally {\n this.testClients.delete(testClient);\n this.testConnectionInFlight = false;\n }\n }\n\n /**\n * Reply an addDelivery failure to the sendTo caller. This is the documented\n * script API envelope (`{success: false, error_message}`) \u2014 unchanged for\n * backward compatibility; only the admin checkConnection uses {error}.\n *\n * @param obj The sendTo message being answered\n * @param message Human-readable failure reason\n */\n private replyAddError(obj: ioBroker.Message, message: string): void {\n this.sendTo(obj.from, obj.command, { success: false, error_message: message }, obj.callback);\n }\n\n /**\n * Script-facing addDelivery command: validate the message shape, cap field\n * lengths, throttle bursts, forward to the API and trigger a poll on\n * success. Extracted from the onMessage switch (M9) \u2014 one command, one\n * method, one change reason.\n *\n * @param obj The sendTo message (validated: command + callback present)\n */\n private async handleAddDelivery(obj: ioBroker.Message): Promise<void> {\n if (!this.client) {\n // v0.4.3 (F4): trace addDelivery-before-init.\n this.log.debug(\"addDelivery: adapter not initialized\");\n this.replyAddError(obj, \"Adapter not initialized\");\n return;\n }\n // v0.7.2: obj.message is `unknown`-shaped \u2014 a script calling\n // sendTo(\"parcelapp\", \"addDelivery\", null) used to surface as an\n // ugly TypeError through the catch instead of a clear validation\n // message. Coerce to a plain object and validate required fields.\n const raw = obj.message;\n const msg = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};\n if (\n typeof msg.tracking_number !== \"string\" ||\n msg.tracking_number.length === 0 ||\n typeof msg.carrier_code !== \"string\" ||\n msg.carrier_code.length === 0 ||\n typeof msg.description !== \"string\" ||\n msg.description.length === 0\n ) {\n this.log.debug(\"addDelivery: missing tracking_number/carrier_code/description in message\");\n this.replyAddError(obj, \"tracking_number, carrier_code and description are required\");\n return;\n }\n // v0.9.0 (S5): cap field lengths so a runaway local script can't push\n // a multi-MB POST body to parcel.app. Checked after the required-field\n // guard above so the two validation messages stay distinct.\n if (\n msg.tracking_number.length > MAX_ADD_FIELD_LEN ||\n msg.carrier_code.length > MAX_ADD_FIELD_LEN ||\n msg.description.length > MAX_ADD_FIELD_LEN ||\n (typeof msg.language === \"string\" && msg.language.length > MAX_ADD_FIELD_LEN)\n ) {\n this.log.debug(\"addDelivery: a field exceeds the maximum length\");\n this.replyAddError(obj, `each field must be at most ${MAX_ADD_FIELD_LEN} characters`);\n return;\n }\n // Pass the optional API fields through when the caller supplies them\n // (language: ISO 639-1 two-letter code; send_push_confirmation: push\n // notification once the delivery is added).\n const request: AddDeliveryRequest = {\n tracking_number: msg.tracking_number,\n carrier_code: msg.carrier_code,\n description: msg.description,\n };\n if (typeof msg.language === \"string\" && msg.language.length > 0) {\n request.language = msg.language;\n }\n if (typeof msg.send_push_confirmation === \"boolean\") {\n request.send_push_confirmation = msg.send_push_confirmation;\n }\n // v0.9.0 (S4): throttle addDelivery POSTs. parcel.app caps ~20/day\n // server-side; this stops a runaway/buggy script from hammering the API\n // with a burst. Record the attempt before the await so concurrent\n // callers count too.\n const nowMs = Date.now();\n this.addTimestamps = this.addTimestamps.filter(t => nowMs - t < ADD_WINDOW_MS);\n if (this.addTimestamps.length >= MAX_ADDS_PER_WINDOW) {\n this.log.warn(`addDelivery throttled: more than ${MAX_ADDS_PER_WINDOW} requests within ${ADD_WINDOW_MS / 1000}s`);\n this.replyAddError(obj, `too many addDelivery requests; max ${MAX_ADDS_PER_WINDOW} per ${ADD_WINDOW_MS / 1000}s`);\n return;\n }\n this.addTimestamps.push(nowMs);\n const addResult = await this.client.addDelivery(request);\n // v0.4.3 (F5): trace addDelivery result with the (flattened) tracking number.\n // v0.10.0 (L9): the drift-guarded isTrueish \u2014 getDeliveries hardens the same\n // API flag; a drifted `success: \"false\"` string must not read as truthy.\n const added = isTrueish(addResult.success);\n this.log.debug(`addDelivery: '${oneLine(request.tracking_number)}' result=${added ? \"ok\" : \"fail\"}`);\n this.sendTo(obj.from, obj.command, addResult, obj.callback);\n if (added) {\n // v0.10.0 (L5): plain poll, no force \u2014 a single add still polls right\n // away (the last poll is usually >60s back), but an add-burst can no\n // longer stack force-GETs past the 20/h API budget. Nothing is lost:\n // the server caches the list ~45-90 min, a fresh package rarely shows\n // tracking data immediately anyway.\n void this.poll().catch(err => this.log.error(`Poll after addDelivery failed: ${errText(err)}`));\n }\n }\n\n private async cleanupObsoleteStates(): Promise<void> {\n // One getObject per adapter start, forever \u2014 deliberately kept (I3/ARCH-24):\n // a one-shot migration marker would cost more mechanics than this read.\n // Drop the list entirely at the next major once 0.1.x installs are gone.\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 * v0.10.0 (M1): the client codes every failure it raises (TIMEOUT,\n * PARSE_ERROR, ABORTED, RATE_LIMITED, \u2026) \u2014 a present machine code always\n * wins. The message-substring sniff only remains for code-less foreign\n * errors, so an API error_message merely CONTAINING \"timeout\" can no longer\n * be misclassified.\n *\n * @param error The error to classify\n */\n private classifyError(error: Error & { code?: string }): string {\n if (error.code) {\n if (NETWORK_ERROR_CODES.has(error.code)) {\n return \"NETWORK\";\n }\n if (error.code === \"ETIMEDOUT\") {\n return \"TIMEOUT\";\n }\n return error.code;\n }\n if (error.message.includes(\"timeout\")) {\n return \"TIMEOUT\";\n }\n return \"UNKNOWN\";\n }\n\n private async poll(): Promise<void> {\n if (this.isPolling || !this.client || !this.stateManager) {\n // v0.10.0 (M4): make the re-entry/uninitialized skip visible like the\n // rate-limit/throttle skips below \u2014 this used to be the one silent spot\n // where \"the adapter does nothing\" left no trace.\n this.log.debug(\"Skipping poll \u2014 already running or not initialized\");\n return;\n }\n // v0.10.0 (L14): local bindings instead of non-null assertions in the\n // closures below \u2014 provable narrowing the compiler checks, robust against\n // a future refactor nulling the fields mid-poll.\n const client = this.client;\n const stateManager = this.stateManager;\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 (also paces the poll after a\n // successful addDelivery \u2014 see handleAddDelivery).\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 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.setStateChangedAsync(\"info.connection\", { val: true, ack: true });\n\n // Split into active (non-delivered) and visible (what gets states)\n const activeDeliveries = deliveries.filter(d => stateManager.parseStatus(d) !== DELIVERED_STATUS_CODE);\n const visibleDeliveries = autoRemoveMode ? activeDeliveries : deliveries;\n\n // v0.4.2 (S3): reset the per-poll collision tracker, then compute every\n // package id in a deterministic sequential pre-pass (stable array order)\n // BEFORE the parallel updates \u2014 collision-suffixing is then deterministic\n // and packageId runs exactly once per delivery instead of twice.\n stateManager.resetPollState();\n const pkgIds = visibleDeliveries.map(d => stateManager.packageId(d));\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 // v0.9.0 (S3): process the updates in bounded batches instead of one\n // broker fan-out for ALL deliveries at once. The keep-set is still every\n // visible pkgId (computed above), so this only caps concurrency \u2014 it never\n // drops a package. A normal poll (a handful of packages) is a single batch.\n if (visibleDeliveries.length > UPDATE_BATCH_SIZE) {\n this.log.debug(`Updating ${visibleDeliveries.length} deliveries in batches of ${UPDATE_BATCH_SIZE}`);\n }\n for (let start = 0; start < visibleDeliveries.length; start += UPDATE_BATCH_SIZE) {\n const batch = visibleDeliveries.slice(start, start + UPDATE_BATCH_SIZE);\n await Promise.all(\n batch.map(async (delivery, offset) => {\n const pkgId = pkgIds[start + offset];\n // Pre-sanitize the externally-sourced strings once for logging. The\n // fields are optional (the API can drop them), so default to \"\" before\n // oneLine flattens them onto a single log line.\n const tracking = oneLine(delivery.tracking_number ?? \"\");\n const carrier = oneLine(delivery.carrier_code ?? \"\");\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 // v0.10.0 (M6): status_code is typed number|string (drift), so it\n // is flattened like the other externally-sourced fields.\n this.log.debug(\n `updateDelivery: '${tracking}' carrier=${carrier} status=${oneLine(String(delivery.status_code))}`,\n );\n const carrierName = await client.getCarrierName(delivery.carrier_code);\n await stateManager.updateDelivery(delivery, carrierName, pkgId);\n this.failedDeliveries.delete(pkgId);\n } catch (err) {\n const msg = errText(err);\n if (this.failedDeliveries.has(pkgId)) {\n this.log.debug(`Failed to update '${tracking}': ${msg}`);\n } else if (this.unloaded) {\n // v0.10.0 (L2): broker teardown mid-batch is expected noise\n // during shutdown, not a per-package warning.\n this.log.debug(`Failed to update '${tracking}' during shutdown: ${msg}`);\n } else {\n this.log.warn(`Failed to update '${tracking}': ${msg}`);\n this.failedDeliveries.add(pkgId);\n }\n }\n }),\n );\n }\n\n // v0.9.0 (C1): keep-set = EVERY package the API still returns this poll\n // (pkgIds), NOT only the writes that just succeeded. A transient\n // updateDelivery failure leaves a package's states stale, but it must not\n // drop the package from cleanup \u2014 that would delete a still-present\n // package's states (and any user-set device name) at green info.connection.\n // v0.10.0 (M2): broker-side failures in cleanup/summary are NOT API\n // failures \u2014 they must neither flip info.connection to false (the GET\n // above just succeeded) nor run through the API error classification.\n try {\n await stateManager.cleanupDeliveries(pkgIds);\n // Update summary (always uses active/non-delivered)\n await stateManager.updateSummary(activeDeliveries);\n } catch (err) {\n this.log.warn(`State maintenance failed (API connection is fine, retrying next poll): ${errText(err)}`);\n }\n\n // Keep failedDeliveries bounded: drop entries for package ids no longer\n // present, so packages that vanish from the API don't linger forever.\n const seenPkgIds = new Set(pkgIds);\n for (const id of [...this.failedDeliveries]) {\n if (!seenPkgIds.has(id)) {\n this.failedDeliveries.delete(id);\n }\n }\n\n this.log.debug(`Polled ${visibleDeliveries.length} deliveries (${activeDeliveries.length} active)`);\n } catch (err) {\n await this.handlePollError(err as Error & { code?: string; retryAfterSeconds?: number });\n } finally {\n this.isPolling = false;\n }\n }\n\n /**\n * Classify + route a poll failure: log level, dedup, cooldown and the\n * info.connection=false write. Extracted from poll()'s catch (M9) so the\n * happy path reads as a plain sequence and the error policy sits next to\n * classifyError. Dispatches on the CLASSIFIED code only (L6) \u2014 one source\n * of truth for the error class.\n *\n * @param error The poll failure (usually an ApiError from the client)\n */\n private async handlePollError(error: Error & { code?: string; retryAfterSeconds?: number }): Promise<void> {\n const errorCode = this.classifyError(error);\n const isRepeat = errorCode === this.lastErrorCode;\n this.lastErrorCode = errorCode;\n\n switch (errorCode) {\n case \"ABORTED\":\n // v0.10.0 (M1): expected during shutdown \u2014 cancelAll aborts the\n // in-flight GET. A deliberate stop must not paint a red error line.\n this.log.debug(`Poll aborted: ${error.message}`);\n break;\n case \"RATE_LIMITED\": {\n // v0.4.2 (M9): clamp Retry-After into [60s, 24h] (shared constants\n // with the client parser, L7). A bogus 0/negative/fractional value\n // must neither wipe the cooldown nor set it for milliseconds.\n const rawCooldown = error.retryAfterSeconds ?? 0;\n const cooldownSec =\n Number.isFinite(rawCooldown) && rawCooldown > 0\n ? Math.min(RETRY_AFTER_MAX_SEC, Math.max(60, Math.floor(rawCooldown)))\n : RETRY_AFTER_DEFAULT_SEC;\n this.rateLimitedUntil = Date.now() + cooldownSec * 1000;\n // v0.10.0 (M3): warn once \u2014 a persistent 429 repeats at debug.\n const line = `Rate limit hit \u2014 pausing API requests for ${Math.ceil(cooldownSec / 60)} minute(s)`;\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.warn(line);\n }\n break;\n }\n case \"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 // v0.10.0 (M3): once at error level, repeats at debug \u2014 not 144\n // identical error lines per day for one unchanged account problem.\n const line =\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 if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"INVALID_API_KEY\": {\n // v0.10.0 (M3): first occurrence at error (the user must fix the\n // config; info.connection goes red too) \u2014 repeats at debug.\n const line = \"Invalid API key \u2014 please check your parcel.app API key\";\n if (isRepeat) {\n this.log.debug(line);\n } else {\n this.log.error(line);\n }\n break;\n }\n case \"NETWORK\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"Cannot reach parcel.app API \u2014 will keep retrying\");\n }\n break;\n case \"TIMEOUT\":\n if (isRepeat) {\n this.log.debug(`Poll failed (ongoing): ${error.message}`);\n } else {\n this.log.warn(\"API request timeout \u2014 will retry next cycle\");\n }\n break;\n default:\n 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 {\n this.log.error(`Poll failed: ${error.message}`);\n }\n }\n\n // C2: setStateChangedAsync avoids redundant `false` writes on sustained\n // failure. The `.catch` keeps poll() from rejecting when the broker is\n // already down, so the fire-and-forget callers never see an unhandled\n // rejection (no global process handler needed).\n await this.setStateChangedAsync(\"info.connection\", { val: false, ack: true }).catch(() => {\n /* broker shutting down \u2014 ignore */\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": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "parcelapp",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.1",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.10.1": {
|
|
7
|
+
"en": "Internal refactoring. No user-facing changes.",
|
|
8
|
+
"de": "Interne Überarbeitung. Keine für Nutzer sichtbaren Änderungen.",
|
|
9
|
+
"ru": "Внутренняя переработка. Без изменений, видимых пользователю.",
|
|
10
|
+
"pt": "Refatoração interna. Sem alterações visíveis para o utilizador.",
|
|
11
|
+
"nl": "Interne herstructurering. Geen zichtbare wijzigingen voor de gebruiker.",
|
|
12
|
+
"fr": "Refactorisation interne. Aucun changement visible pour l'utilisateur.",
|
|
13
|
+
"it": "Refactoring interno. Nessuna modifica visibile per l'utente.",
|
|
14
|
+
"es": "Refactorización interna. Sin cambios visibles para el usuario.",
|
|
15
|
+
"pl": "Wewnętrzna refaktoryzacja. Brak zmian widocznych dla użytkownika.",
|
|
16
|
+
"uk": "Внутрішня переробка. Без видимих для користувача змін.",
|
|
17
|
+
"zh-cn": "内部重构。对用户无可见变化。"
|
|
18
|
+
},
|
|
6
19
|
"0.10.0": {
|
|
7
20
|
"en": "Admin connection test reports real errors now.\nQuieter logs, steadier connection state and automatic recovery from stalled requests or failed starts.",
|
|
8
21
|
"de": "Der Verbindungstest im Admin meldet jetzt echte Fehler.\nRuhigere Logs, stabile Verbindungsanzeige, automatische Erholung bei hängenden Anfragen und Startfehlern.",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Dodano opcjonalne raportowanie błędów przez Sentry: awarie są wysyłane do dewelopera, aby szybciej rozwiązywać problemy. Aktywne tylko przy włączonej diagnostyce ioBroker; anonimowo.",
|
|
81
94
|
"uk": "Додано необов'язкову відправку помилок через Sentry: збої надсилаються розробнику, щоб швидше їх виправляти. Працює лише з увімкненою діагностикою ioBroker; анонімно.",
|
|
82
95
|
"zh-cn": "新增可选的 Sentry 错误上报:崩溃信息会发送给开发者以更快修复问题。仅在启用 ioBroker 诊断时生效;匿名。"
|
|
83
|
-
},
|
|
84
|
-
"0.6.0": {
|
|
85
|
-
"en": "Bug fixes: correct combined delivery window, packages with an unknown status stay visible, and newly added deliveries appear immediately.",
|
|
86
|
-
"de": "Fehlerbehebungen: korrektes kombiniertes Lieferfenster, Pakete mit unbekanntem Status bleiben sichtbar, und neue Sendungen erscheinen sofort.",
|
|
87
|
-
"ru": "Исправления: корректное объединённое окно доставки, посылки с неизвестным статусом остаются видимыми, новые посылки появляются сразу.",
|
|
88
|
-
"pt": "Correções: janela de entrega combinada correta, encomendas com estado desconhecido permanecem visíveis e novas encomendas aparecem de imediato.",
|
|
89
|
-
"nl": "Bugfixes: correct gecombineerd bezorgvenster, pakketten met onbekende status blijven zichtbaar en nieuwe zendingen verschijnen direct.",
|
|
90
|
-
"fr": "Corrections : fenêtre de livraison combinée correcte, les colis au statut inconnu restent visibles et les nouvelles livraisons apparaissent immédiatement.",
|
|
91
|
-
"it": "Correzioni: finestra di consegna combinata corretta, i pacchi con stato sconosciuto restano visibili e le nuove spedizioni compaiono subito.",
|
|
92
|
-
"es": "Correcciones: ventana de entrega combinada correcta, los paquetes con estado desconocido siguen visibles y los envíos nuevos aparecen al instante.",
|
|
93
|
-
"pl": "Poprawki: poprawne łączone okno dostawy, paczki o nieznanym statusie pozostają widoczne, a nowe przesyłki pojawiają się natychmiast.",
|
|
94
|
-
"uk": "Виправлення: коректне об'єднане вікно доставки, посилки з невідомим статусом залишаються видимими, нові посилки з'являються одразу.",
|
|
95
|
-
"zh-cn": "错误修复:合并送达窗口计算正确,状态未知的包裹保持可见,新添加的包裹立即显示。"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"plugins": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.parcelapp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "ioBroker adapter for the parcel.app API",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "krobi",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"node": ">=22"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@iobroker/adapter-core": "^3.4.
|
|
33
|
+
"@iobroker/adapter-core": "^3.4.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@alcalzone/release-script": "^5.2.1",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@alcalzone/release-script-plugin-license": "^5.2.0",
|
|
39
39
|
"@iobroker/adapter-dev": "^1.5.0",
|
|
40
40
|
"@iobroker/eslint-config": "^2.3.4",
|
|
41
|
-
"@iobroker/testing": "^5.
|
|
41
|
+
"@iobroker/testing": "^5.3.0",
|
|
42
42
|
"@tsconfig/node22": "^22.0.5",
|
|
43
43
|
"@types/iobroker": "npm:@iobroker/types@^7.2.2",
|
|
44
44
|
"@types/node": "^22.19.17",
|