]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
refactor(ocpp): extract per-station state plumbing into shared OCPPIncomingRequestSer...
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 8 Jul 2026 19:45:50 +0000 (21:45 +0200)
committerGitHub <noreply@github.com>
Wed, 8 Jul 2026 19:45:50 +0000 (21:45 +0200)
commit0fff8b2a611829b5f996aae0ebc4d50883eb0632
treedd79bd820a75ab70423ae299a026eafbd6731e35
parente406ab2ab278ee72b49bb1e87ec5fe70c321b9bc
refactor(ocpp): extract per-station state plumbing into shared OCPPIncomingRequestService base (#1983)

Closes #1963. Companion issues #1971, #1972, #1973 will consume this base.

Pure structural refactor + OCPP 1.6 concretization. Zero observable
behavior change on OCPP 2.0.1: every field currently cleared in the
former OCPP20IncomingRequestService.stop() body is still cleared, in
the same order, at the same lifecycle point.

Touchpoints:

A. src/charging-station/ocpp/OCPPIncomingRequestService.ts
   Base becomes generic <TStationState extends object = object>. Adds
   shared 'stationsState' WeakMap (L86), 'getOrCreateStationState'
   lazy-init, concrete 'stop()' template (bound in the constructor at
   L94 to prevent detach-and-call regressions), and abstract hooks
   'createStationState' / 'resetStationState'. Documents the
   exception-safety contract on the template: a throw in
   'resetStationState' skips WeakMap eviction and any subclass
   extension after 'super.stop()' — matches pre-refactor semantics.
   The '= object' default on the generic parameter is load-bearing
   (removing it would break the static registry Map and the
   getInstance constraint with TS2314).

B. src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
   Extends OCPPIncomingRequestService<OCPP20StationState>. Removes
   local 'stationsState' field and 'getOrCreateStationState'. Adds
   'createStationState' factory and 'resetStationState' override with
   the 5-statement ordering invariant of the pre-refactor stop() body.
   The abort-before-clear ordering matters because clearing first
   makes the subsequent '?.abort()' short-circuit on the nulled field,
   leaving the in-flight operation un-signaled. 'stop()' override
   calls super first, then keeps the unconditional
   OCPP20VariableManager cleanup. Class-level JSDoc documents OCPP 2.1
   subclass path (extends OCPP20IncomingRequestService inherits state,
   reset, and stop() unchanged; widening the generic parameter for new
   fields requires a preparatory refactor with a factory cast per
   TS2352, or subclass override of createStationState).

C. src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
   Adds empty 'OCPP16StationState' interface. Extends
   OCPPIncomingRequestService<OCPP16StationState>. Adds no-op
   'createStationState' + 'resetStationState' overrides. Removes the
   '/* no-op for OCPP 1.6 */' stop() override (now inherited from the
   base template). The 'resetStationState' JSDoc prescribes the
   abort-before-clear ordering companion issues MUST follow.

D. eslint.config.js
   Adds 'no-restricted-syntax' rule enforcing the INVARIANT: forbids
   direct '.set/.delete/.clear' calls on 'stationsState' from outside
   the base class file, forbids aliasing 'stationsState' to a local
   binding, and forbids destructuring 'stationsState'. Covers the AST
   escape hatches identified during hostile-adversarial review.
   Enforced by 'pnpm lint' in CI on every companion PR.

E. src/charging-station/ocpp/OCPPServiceUtils.ts
   Adds bidirectional cross-reference in the comment on
   'warnedInvalidMeasurands' distinguishing the module-scope warn-once
   diagnostic cache from the class-scope lifecycle-state WeakMap on
   OCPPIncomingRequestService.stationsState.

Companion issues consume this base infrastructure in order:
  - #1971 GetDiagnostics supersession -> activeDiagnosticsAbortController + Id
  - #1972 firmware setTimeout cancel  -> deferred firmware timer handle
  - #1973 trigger cross-check         -> activeFirmwareUpdateRequestId

#1971 and #1973 share 'activeDiagnosticsRequestId': whichever lands
first adds the (optional) field to OCPP16StationState; the second
lands as-is.

Line-count delta (plumbing moved, not duplicated):
  Base   :  182 -> 290 (+108) plumbing + docs (~+80 JSDoc / +28 code)
  OCPP20 : 4579 -> 4627  (+48) 22 lines of plumbing removed; docs +55,
                                 net code -7 (concrete overrides only)
  OCPP16 : 1958 -> 1985  (+27) interface + 2 concrete implementations
                                 + companion guidance
  eslint : 172 -> 198   (+26) 3-selector no-restricted-syntax rule
  utils  : 2000 -> 2004  (+4)  bidirectional cross-ref comment
  test   : new file (+150) 7 plumbing tests

Origin of the OCPP 2.0.1 pattern being harmonized:
  - 47cdf2bbe refactor(ocpp20): isolate per-station state with
    WeakMap instead of singleton properties (introduces the WeakMap
    pattern with initial names 'OCPP20PerStationState' / 'stationStates'
    / 'getStationState')
  - f1e33ea42 refactor(ocpp20): harmonize per-station state naming
    (renames to 'OCPP20StationState' / 'stationsState')
  - 17396c1c4 refactor(ocpp20): rename getStationState to
    getOrCreateStationState (lazy-getter naming split)
  - c0b25553 fix(ocpp20): cancel cert-signing retry timer in stop()
  - be29b4c8 fix(ocpp20): add AbortController to log-upload lifecycle
    + stop() abort
  - d2e9b8b0 refactor(ocpp20): extract resetActiveFirmwareUpdateState
    helper
  - ce875dae refactor(ocpp20): harmonize firmware-state cleanup + log
    superseded

Tests: 7 new plumbing tests in
tests/charging-station/ocpp/OCPPIncomingRequestService-StationState.test.ts
covering lazy-init idempotency (with createStationState spy verifying
call count = 1 after two getOrCreate calls), WeakMap eviction on
stop(), resetStationState invocation count, no-op guard when no state
exists, two-station isolation, the OCPP 1.6 default resetStationState
no-op (with reference identity check), and the exception-safety
contract lock (throw in resetStationState skips WeakMap.delete, and
subsequent stop() re-invokes on same state before evicting). Zero
edits to existing OCPP 2.0.1 tests.

The OCPP 2.0.1 5-statement ordering invariant is enforced by four
independent mechanisms: (1) JSDoc rationale on
OCPP20IncomingRequestService.resetStationState documenting the
'?.abort()' short-circuit consequence of reordering, (2) the
'no-restricted-syntax' ESLint rule preventing direct 'stationsState'
mutations from subclass code (with selector coverage for aliasing and
destructuring bypasses; enforced in CI), (3) constructor
'stop.bind(this)' defense-in-depth against detach-and-call
regressions, and (4) byte-identical preservation of the pre-refactor
stop() body.
eslint.config.js
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/OCPPIncomingRequestService.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
tests/charging-station/ocpp/OCPPIncomingRequestService-StationState.test.ts [new file with mode: 0644]