fix(ocpp): implement OCPP 1.6 GetDiagnostics supersession via AbortController (#1991)
Closes #1971
The entry guard added by PR #1987 at handleRequestGetDiagnostics
prevented concurrent FTP uploads racing on the same
${chargingStationId}_logs.tar.gz archive by silently dropping the
second GetDiagnostics.req with an empty GetDiagnostics.conf. That
mitigated the concrete race but was not the OCPP 1.6 supersession
semantic: the new request needed to abort the in-flight upload and
start a fresh one, with a terminal DiagnosticsStatusNotification
emitted for the superseded upload.
Refactor the entry guard from skip-second into abort-then-install:
- Add `activeDiagnosticsAbortController?: AbortController` to
`OCPP16StationState` (alphabetical, above the existing fields).
- On supersession, the entry guard calls
`stationState.activeDiagnosticsAbortController?.abort()` and
installs a fresh controller. `diagnosticsUploadInProgress` stays
`true` across the handoff so the §4.4 / §7.24 trigger cross-check
never observes a false-idle window.
- `basic-ftp` v6 does not natively consume `AbortSignal`; the
documented interruption path is `Client.close()`, which invokes
`FtpContext.close()` and rejects the pending `uploadFrom` task
with `User closed client during task`. The handler wires
`signal.addEventListener('abort', () => ftpClient?.close(), { once: true })`.
- The existing `catch` at L1390 already emits
`DiagnosticsStatusNotification(UploadFailed)` for any thrown error,
so a close-triggered rejection flows through unchanged — no second
emission needed. OCPP 1.6 `DiagnosticsStatusNotification.req` (§6.17)
does not carry a `requestId`; the terminal for the superseded upload
is uncorrelated by design, only its temporal position tells the CSMS
which upload it belonged to.
- The `finally` clause identity-guards its cleanup
(`if (stationState.activeDiagnosticsAbortController === abortController)`)
so a superseded handler's late unwind cannot clobber the new
lifecycle's state. Cleaner than the microtask-yield primitive
sketched in the initial design because it removes the ordering
dependency between the two handlers' async continuations.
- `resetStationState` aborts the in-flight controller before the base
template deletes the WeakMap entry, following the cancel-before-delete
ordering documented on `OCPP20IncomingRequestService.resetStationState`.
Mirrors the OCPP 2.0.1 log-upload supersession pattern (commits
29a8330f,
ac6ed218,
64f384fa,
be29b4c8) but does not import
`AcceptedCanceled` — that response semantic is a 2.0.1 concept absent
from OCPP 1.6 `GetDiagnostics.conf` (§6.26).