Skip to content

Commit 0c3f1e5

Browse files
Yerazeclaude
andcommitted
refactor(dist-delete): unify distance-delete ownership; ISourceManager lifecycle (#3962 task 2.2d)
WP1 — Widen ISourceManager with required startDistanceDeleteScheduler() / stopDistanceDeleteScheduler() members. All 4 concrete managers already exposed these methods; this formalizes the contract so server.ts callbacks can be typed rather than duck-cast. Updates 4 test stub objects to satisfy the widened interface. WP2 — MeshtasticManager adopts the shared DistanceDeleteScheduler class, deleting the inline setInterval / distanceDeleteInterval field (field + 3 clear sites removed). Fixes a latent bug where the inline stopDistanceDeleteScheduler() discarded the 2-min initial setTimeout handle, letting a stray runDeleteCycle fire if auto-delete was disabled within the connect window. Implementation deviation: uses a lazy dynamic import() instead of a top-level static import to break a circular module chain that Vitest's CJS-like transform cannot resolve with live bindings: meshtasticManager → distanceDeleteScheduler → autoDeleteByDistanceService → resolveSourceManager → meshtasticManager Field becomes `DistanceDeleteScheduler | null` (lazy), identical runtime behavior. WP3 — server.ts distance-delete callbacks drop the `as { startDistanceDeleteScheduler?: () => Promise<void> }` duck-casts and call typed ISourceManager members directly. Stale "across both the primary and MeshCore registries" comment corrected (single unified registry since task 2.1). Ride-along: fix stale comment in meshtasticManager.autoresponder-regex.test.ts (src/utils/autoResponderUtils.ts → src/server/utils/autoResponderMatcher.ts, promised on PR #4011). New test: meshtasticManager.distanceDelete.test.ts (5 tests: lazy-null field before start, delegation via prototype spies, no-op stop, stop-after-start, regression timer-cancel that locks in the latent-bug fix). Epic: 2.2c log entry + 2.2d log entry added to REMEDIATION_EPIC.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AphLfgUJWaFNAgU5UXdJ4n
1 parent 9b972b8 commit 0c3f1e5

9 files changed

Lines changed: 194 additions & 54 deletions

docs/internal/dev-notes/REMEDIATION_EPIC.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Phase 2 started 2026-07-07. Decisions carried forward from the Phase 0+1 intervi
3535
- [x] **2.2a** — Heartbeat/status-probing: extract shared service parameterized by `ISourceManager` (plan §2.2 ¶1).
3636
- [x] **2.2b** — Auto-announce: MeshCore + Meshtastic cycles → one `autoAnnounceService` with per-protocol adapters (plan §2.2 ¶2).
3737
- [x] **2.2c** — Auto-responder: `checkAutoResponder` in both managers → shared service (plan §2.2 ¶3).
38-
- [ ] **2.2d** — Distance-delete scheduling: MeshCore's `DistanceDeleteScheduler` construction → unified ownership in `services/` (plan §2.2 ¶4).
38+
- [x] **2.2d** — Distance-delete scheduling: MeshCore's `DistanceDeleteScheduler` construction → unified ownership in `services/` (plan §2.2 ¶4).
3939
- [ ] **2.3** — Singleton retirement: enumerate legacy-singleton branches in `meshtasticManager.ts`; create a registry-managed default source from env config; reduce `export default` to a pure alias; delete last special-casing (plan §2.3).
4040

4141
## Ordering notes
@@ -65,3 +65,4 @@ Record per-phase: PR link, deviations from plan, follow-ups.
6565
- **2.2a** — PR #4005 merged (HeartbeatScheduler leaf; MeshCore adopts; premise correction documented — no cross-protocol heartbeat duplication). Premise correction: no cross-protocol heartbeat duplication exists (Meshtastic = transport-level keepalive push; MQTT = none) — reduced scope to a HeartbeatScheduler leaf utility adopted by MeshCore only; reconnect machinery untouched (Phase 4.2). New files: `src/server/services/heartbeatScheduler.ts` (protocol-agnostic scheduler: interval/start/stop/in-flight-guard/pre+post-await connected-gate) + `heartbeatScheduler.test.ts` (8 fake-timer unit tests covering every gate). MeshCore adoption: `startHeartbeat`/`stopHeartbeat` delegate to per-instance HeartbeatScheduler; `runHeartbeatProbe` deleted; `heartbeatTimer`/`heartbeatProbeInFlight` fields removed; `heartbeatProbe`/`onHeartbeatOk` added as manager callbacks; `recordHeartbeatFailure`/`beginReconnect` chain unchanged. New integration test in meshcoreNativeBackend.test.ts: N consecutive probe failures → heartbeat_failed×N + state='reconnecting'.
6666
- **2.2b** — PR #4009 merged (CronOrIntervalScheduler leaf; both managers delegate arming; premise correction: ~40 LOC shared skeleton, adapter service = negative ROI). Ride-along: hotfix PR #4007 fixed main broken by #4004/#4002 merge race.
6767
- **2.2c** — Premise correction #3 (cross-manager duplication ≈0%; real find = Meshtastic matcher copy-pasted into its own test — now extracted so tests guard production; MeshCore intentionally separate). Extracted the ~130-line placeholder-DSL matcher (`{name}` / `{name:regex}` brace-parse → capture regex → param extraction) from `checkAutoResponder` in `meshtasticManager.ts` into `matchAutoResponderPattern()` + `AutoResponderMatch` interface in `src/utils/autoResponderUtils.ts`. `meshtasticManager.ts` now calls `matchAutoResponderPattern` (3-line loop); removed orphaned `normalizedText`. Retargeted `meshtasticManager.autoresponder-regex.test.ts` (621 lines) to import the production function — deleted local `extractParameters`+`testTriggerMatch` copy (lines 16–155), adapted all ~61 assertions to `{matched,params}`; added 1 explicit `{matched:false,params:{}}` edge-case assertion. MeshCore `checkAutoResponder` byte-for-byte unchanged. Full suite: success=true, 0 failed, 0 suitesFailed; typecheck clean; typecheck:tests 283 errors (at baseline); lint:ci exit 0.
68+
- **2.2d** — Premise correction #4 (plan inverted — shared class `DistanceDeleteScheduler` already existed for 3/4 managers; MeshtasticManager was the outlier with inline copy). WP1: `ISourceManager` widened with required `startDistanceDeleteScheduler(): Promise<void>` + `stopDistanceDeleteScheduler(): void` (all 4 concrete managers already implemented them — this is formalization; 4 test stubs updated). WP2: MeshtasticManager migrates from inline `setInterval`/`distanceDeleteInterval` (field + 3 clear sites) to shared `DistanceDeleteScheduler`; fixes latent uncancellable-initial-run bug (discarded setTimeout handle — shared class's `stop()` cancels it). **Deviation from spec field-layout:** `private readonly distanceDeleteScheduler` changed to `private distanceDeleteScheduler: DistanceDeleteScheduler | null = null` with lazy init in `startDistanceDeleteScheduler()` via dynamic `import()`. Root cause: new static import `meshtasticManager → distanceDeleteScheduler` closed a circular chain (`distanceDeleteScheduler → autoDeleteByDistanceService → resolveSourceManager → meshtasticManager`) that Vitest's CJS-like transform cannot resolve with live bindings — 10 test suite collection failures. Lazy `await import()` breaks the static cycle while delivering identical runtime behavior. WP3: `server.ts` duck-casts removed (`as { startDistanceDeleteScheduler?: ... }`) — now typed `ISourceManager` calls; stale "both registries" comment fixed. Ride-along: `meshtasticManager.autoresponder-regex.test.ts` header comment corrected (`src/utils/autoResponderUtils.ts` → `src/server/utils/autoResponderMatcher.ts`). New test: `meshtasticManager.distanceDelete.test.ts` (5 tests: lazy-null assert, delegation via prototype spies, no-op stop, stop-after-start, regression timer-cancel). Full suite: success=true, 8343 passed, 0 failed, 0 suitesFailed; typecheck clean; typecheck:tests 283 (at baseline); lint:ci exit 0 (3 rules improved by cast removal); build succeeded.

src/server/meshcoreRegistry.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ function makeMeshCoreStub(sourceId = 'mc-shim'): ISourceManager {
3030
connected: true,
3131
}),
3232
getLocalNodeInfo: () => null,
33+
startDistanceDeleteScheduler: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
34+
stopDistanceDeleteScheduler: vi.fn<() => void>(),
3335
};
3436
}
3537

@@ -53,6 +55,8 @@ function makeMeshtasticStub(sourceId = 'mt-shim'): ISourceManager {
5355
longName: 'Node',
5456
shortName: 'N',
5557
}),
58+
startDistanceDeleteScheduler: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
59+
stopDistanceDeleteScheduler: vi.fn<() => void>(),
5660
};
5761
}
5862

src/server/meshtasticManager.autoresponder-regex.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { matchAutoResponderPattern } from './utils/autoResponderMatcher.js';
99
* with custom regex patterns using {param:regex} syntax.
1010
*
1111
* All assertions run against the PRODUCTION `matchAutoResponderPattern` function
12-
* (src/utils/autoResponderUtils.ts) — not a local copy.
12+
* (src/server/utils/autoResponderMatcher.ts) — not a local copy.
1313
*/
1414

1515
describe('Auto Responder - Regex Parameter Matching', () => {
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* Unit tests for MeshtasticManager's adoption of DistanceDeleteScheduler (#3962 task 2.2d).
3+
*
4+
* Verifies:
5+
* - MeshtasticManager creates a DistanceDeleteScheduler lazily on the first
6+
* startDistanceDeleteScheduler() call and delegates lifecycle methods.
7+
* - Regression: stop() within the 2-minute initial-run window cancels the pending
8+
* setTimeout, so no runDeleteCycle fires when auto-delete is disabled quickly after
9+
* connect (latent bug in the prior inline implementation where the handle was discarded).
10+
*
11+
* Implementation note: MeshtasticManager uses a dynamic import() for DistanceDeleteScheduler
12+
* to avoid a static circular dependency:
13+
* meshtasticManager → distanceDeleteScheduler → autoDeleteByDistanceService
14+
* → resolveSourceManager → meshtasticManager
15+
* The lazy-init pattern is transparent to callers and preserves all runtime behavior.
16+
*
17+
* Uses the same minimal-mock pattern as meshtasticManager.reconnectAddress.test.ts.
18+
*/
19+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
20+
21+
// --- Stubs (minimal — mirrors meshtasticManager.reconnectAddress.test.ts) -------
22+
23+
vi.mock('./tcpTransport.js', () => ({
24+
TcpTransport: class {
25+
connect = vi.fn().mockResolvedValue(undefined);
26+
disconnect = vi.fn().mockResolvedValue(undefined);
27+
send = vi.fn().mockResolvedValue(undefined);
28+
on = vi.fn();
29+
off = vi.fn();
30+
removeAllListeners = vi.fn();
31+
isConnected = () => false;
32+
setStaleConnectionTimeout = vi.fn();
33+
setConnectTimeout = vi.fn();
34+
setReconnectTiming = vi.fn();
35+
},
36+
}));
37+
38+
const getSettingForSource = vi.fn();
39+
vi.mock('../services/database.js', () => {
40+
const shared = {
41+
waitForReady: vi.fn().mockResolvedValue(undefined),
42+
settings: {
43+
getSetting: vi.fn().mockResolvedValue(null),
44+
setSetting: vi.fn().mockResolvedValue(undefined),
45+
getSettingForSource: (...args: unknown[]) => getSettingForSource(...args),
46+
},
47+
sources: { getSource: vi.fn().mockResolvedValue(null) },
48+
nodes: {
49+
getNode: vi.fn().mockResolvedValue(null),
50+
getAllNodes: vi.fn().mockResolvedValue([]),
51+
getActiveNodes: vi.fn().mockResolvedValue([]),
52+
},
53+
getAllTraceroutesForRecalculationAsync: vi.fn().mockResolvedValue([]),
54+
recordTracerouteRequestAsync: vi.fn().mockResolvedValue(undefined),
55+
};
56+
return { default: shared, databaseService: shared };
57+
});
58+
59+
// --- Imports (after vi.mock hoisting) ------------------------------------------
60+
61+
import { MeshtasticManager } from './meshtasticManager.js';
62+
import { DistanceDeleteScheduler } from './services/distanceDeleteScheduler.js';
63+
import { autoDeleteByDistanceService } from './services/autoDeleteByDistanceService.js';
64+
65+
// -------------------------------------------------------------------------------
66+
67+
describe('MeshtasticManager — DistanceDeleteScheduler adoption (#3962 task 2.2d)', () => {
68+
beforeEach(() => {
69+
vi.useFakeTimers();
70+
getSettingForSource.mockReset();
71+
// Default: disabled so scheduler creation doesn't trigger DB reads unnecessarily.
72+
getSettingForSource.mockResolvedValue(null);
73+
});
74+
75+
afterEach(() => {
76+
vi.clearAllTimers();
77+
vi.useRealTimers();
78+
vi.restoreAllMocks();
79+
});
80+
81+
it('distanceDeleteScheduler field is null before any start() call (lazy init)', () => {
82+
const mgr = new MeshtasticManager('test-source-1');
83+
// Lazy — null until startDistanceDeleteScheduler() creates it.
84+
expect((mgr as any).distanceDeleteScheduler).toBeNull();
85+
});
86+
87+
it('startDistanceDeleteScheduler() creates a DistanceDeleteScheduler and delegates to start()', async () => {
88+
getSettingForSource.mockImplementation((_: string, key: string) =>
89+
Promise.resolve(key === 'autoDeleteByDistanceEnabled' ? 'false' : null));
90+
91+
// Spy on the prototype so we catch the instance's start() call regardless
92+
// of when the instance is created.
93+
const startSpy = vi.spyOn(DistanceDeleteScheduler.prototype, 'start').mockResolvedValue(undefined);
94+
95+
const mgr = new MeshtasticManager('test-source-2');
96+
await mgr.startDistanceDeleteScheduler();
97+
98+
expect((mgr as any).distanceDeleteScheduler).toBeInstanceOf(DistanceDeleteScheduler);
99+
expect(startSpy).toHaveBeenCalledTimes(1);
100+
});
101+
102+
it('stopDistanceDeleteScheduler() is a no-op when never started', () => {
103+
const mgr = new MeshtasticManager('test-source-3a');
104+
// Must not throw even when the scheduler was never created.
105+
expect(() => mgr.stopDistanceDeleteScheduler()).not.toThrow();
106+
});
107+
108+
it('stopDistanceDeleteScheduler() delegates to the scheduler stop() after a start', async () => {
109+
getSettingForSource.mockImplementation((_: string, key: string) =>
110+
Promise.resolve(key === 'autoDeleteByDistanceEnabled' ? 'false' : null));
111+
112+
vi.spyOn(DistanceDeleteScheduler.prototype, 'start').mockResolvedValue(undefined);
113+
const stopSpy = vi.spyOn(DistanceDeleteScheduler.prototype, 'stop').mockImplementation(() => {});
114+
115+
const mgr = new MeshtasticManager('test-source-3b');
116+
await mgr.startDistanceDeleteScheduler(); // creates scheduler
117+
mgr.stopDistanceDeleteScheduler();
118+
119+
expect(stopSpy).toHaveBeenCalledTimes(1);
120+
});
121+
122+
it('regression: stop() within the 2-minute initial window cancels the pending run (latent inline bug fix)', async () => {
123+
// Spy on the underlying service to detect any stray delete-cycle fires.
124+
const runSpy = vi.spyOn(autoDeleteByDistanceService, 'runDeleteCycle')
125+
.mockResolvedValue({ deletedCount: 0 });
126+
127+
// Configure: auto-delete enabled with a 1-hour interval.
128+
getSettingForSource.mockImplementation((_sourceId: string, key: string) =>
129+
Promise.resolve(key === 'autoDeleteByDistanceEnabled' ? 'true' : '1'));
130+
131+
const mgr = new MeshtasticManager('test-source-reg');
132+
133+
// ARM — creates and starts the DistanceDeleteScheduler (simulates the connect path).
134+
await mgr.startDistanceDeleteScheduler();
135+
136+
// Advance 30 s — still inside the 2-minute initial window; no run yet.
137+
await vi.advanceTimersByTimeAsync(30_000);
138+
expect(runSpy).not.toHaveBeenCalled();
139+
140+
// DISARM before the initial run fires — shared class cancels initialTimeout.
141+
mgr.stopDistanceDeleteScheduler();
142+
143+
// Advance well past 2 minutes AND one interval tick.
144+
await vi.advanceTimersByTimeAsync(2 * 60 * 60 * 1000); // 2h
145+
146+
// No stray fire: the shared DistanceDeleteScheduler.stop() cancels initialTimeout.
147+
// The prior inline implementation discarded the setTimeout handle (latent bug).
148+
expect(runSpy).not.toHaveBeenCalled();
149+
});
150+
});

src/server/meshtasticManager.ts

Lines changed: 23 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ import {
3131
type ParsedClientNotification,
3232
} from './services/clientNotificationPolicy.js';
3333
import { waypointService } from './services/waypointService.js';
34-
import { autoDeleteByDistanceService } from './services/autoDeleteByDistanceService.js';
34+
// import type only — cannot use a static runtime import here because of a circular
35+
// dependency chain: meshtasticManager → distanceDeleteScheduler →
36+
// autoDeleteByDistanceService → resolveSourceManager → meshtasticManager.
37+
// The class is loaded lazily via dynamic import() inside startDistanceDeleteScheduler().
38+
import type { DistanceDeleteScheduler } from './services/distanceDeleteScheduler.js';
3539
import { MessageQueueService } from './messageQueueService.js';
3640
import { resolveAutoWelcomeDelaySeconds } from './autoWelcomeDelay.js';
3741
import { resolveAutoAckPreSendDelaySeconds } from './autoAckDelay.js';
@@ -530,7 +534,9 @@ class MeshtasticManager implements ISourceManager {
530534
private userDisconnectedState = false; // Track user-initiated disconnect
531535
private tracerouteInterval: NodeJS.Timeout | null = null;
532536
private tracerouteJitterTimeout: NodeJS.Timeout | null = null;
533-
private distanceDeleteInterval: NodeJS.Timeout | null = null;
537+
// null until startDistanceDeleteScheduler() is first called (lazy-loaded to
538+
// avoid the meshtasticManager → distanceDeleteScheduler circular import).
539+
private distanceDeleteScheduler: DistanceDeleteScheduler | null = null;
534540
// Reconnect flood prevention timing (#2474)
535541
private static readonly SCHEDULER_STAGGER_MS = 5000; // Delay between each scheduler start
536542
private static readonly CONFIG_COMPLETE_FALLBACK_MS = 120000; // Fallback if configComplete never arrives
@@ -2019,51 +2025,26 @@ class MeshtasticManager implements ISourceManager {
20192025
}, initialJitterMs);
20202026
}
20212027

2022-
/**
2023-
* Start (or restart) the per-source auto-delete-by-distance scheduler.
2024-
* Reads autoDeleteByDistanceEnabled / autoDeleteByDistanceIntervalHours via
2025-
* getSettingForSource so each source uses its own configuration.
2028+
/** Start this source's per-source auto-delete-by-distance scheduler (#3901).
2029+
*
2030+
* The DistanceDeleteScheduler is created lazily on the first call to break the
2031+
* static import cycle:
2032+
* meshtasticManager → distanceDeleteScheduler → autoDeleteByDistanceService
2033+
* → resolveSourceManager → meshtasticManager
2034+
* Subsequent calls re-arm the same instance (DistanceDeleteScheduler.start()
2035+
* calls stop() internally before re-scheduling, so restarts are safe).
20262036
*/
20272037
public async startDistanceDeleteScheduler(): Promise<void> {
2028-
// Clear any existing interval
2029-
if (this.distanceDeleteInterval) {
2030-
clearInterval(this.distanceDeleteInterval);
2031-
this.distanceDeleteInterval = null;
2032-
}
2033-
2034-
const enabled = await databaseService.settings.getSettingForSource(this.sourceId, 'autoDeleteByDistanceEnabled');
2035-
if (enabled !== 'true') {
2036-
logger.debug(`🗑️ Auto-delete-by-distance disabled for source ${this.sourceId}`);
2037-
return;
2038+
if (!this.distanceDeleteScheduler) {
2039+
const { DistanceDeleteScheduler: Scheduler } = await import('./services/distanceDeleteScheduler.js');
2040+
this.distanceDeleteScheduler = new Scheduler(this.sourceId);
20382041
}
2039-
2040-
const intervalHoursStr = await databaseService.settings.getSettingForSource(this.sourceId, 'autoDeleteByDistanceIntervalHours');
2041-
const intervalHours = parseInt(intervalHoursStr || '24', 10);
2042-
const intervalMs = Math.max(1, intervalHours) * 60 * 60 * 1000;
2043-
2044-
logger.debug(`🗑️ Starting auto-delete-by-distance scheduler for source ${this.sourceId} (interval: ${intervalHours}h)`);
2045-
2046-
// Initial run after 2 minutes (matches prior singleton behavior)
2047-
setTimeout(() => {
2048-
autoDeleteByDistanceService.runDeleteCycle(this.sourceId).catch(err =>
2049-
logger.error(`❌ Auto-delete-by-distance initial run failed for source ${this.sourceId}:`, err));
2050-
}, 120_000);
2051-
2052-
this.distanceDeleteInterval = setInterval(() => {
2053-
autoDeleteByDistanceService.runDeleteCycle(this.sourceId).catch(err =>
2054-
logger.error(`❌ Auto-delete-by-distance run failed for source ${this.sourceId}:`, err));
2055-
}, intervalMs);
2042+
await this.distanceDeleteScheduler.start();
20562043
}
20572044

2058-
/**
2059-
* Stop the auto-delete-by-distance scheduler for this source.
2060-
*/
2045+
/** Stop this source's per-source auto-delete-by-distance scheduler (#3901). */
20612046
public stopDistanceDeleteScheduler(): void {
2062-
if (this.distanceDeleteInterval) {
2063-
clearInterval(this.distanceDeleteInterval);
2064-
this.distanceDeleteInterval = null;
2065-
logger.debug(`⏹️ Auto-delete-by-distance scheduler stopped for source ${this.sourceId}`);
2066-
}
2047+
this.distanceDeleteScheduler?.stop();
20672048
}
20682049

20692050
setTracerouteInterval(minutes: number): void {
@@ -14374,10 +14355,7 @@ class MeshtasticManager implements ISourceManager {
1437414355
this.remoteLocalStatsInterval = null;
1437514356
}
1437614357

14377-
if (this.distanceDeleteInterval) {
14378-
clearInterval(this.distanceDeleteInterval);
14379-
this.distanceDeleteInterval = null;
14380-
}
14358+
this.distanceDeleteScheduler?.stop();
1438114359

1438214360
if (this.remoteAdminScannerInterval) {
1438314361
clearInterval(this.remoteAdminScannerInterval);

src/server/server.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,19 +1157,18 @@ setSettingsCallbacks({
11571157
handleAutoWelcomeEnabled: () => { databaseService.handleAutoWelcomeEnabledAsync().catch(() => {}); return 0; },
11581158
invalidateHtmlCache,
11591159
// Auto-delete-by-distance is per-source (#3901): route the restart/stop to
1160-
// the owning source manager (across both the primary and MeshCore registries)
1161-
// so each source schedules against its own settings. There is no global
1162-
// singleton — a null sourceId is a no-op.
1160+
// the owning source manager so each source schedules against its own settings.
1161+
// There is no global singleton — a null sourceId is a no-op.
11631162
restartAutoDeleteByDistanceService: (sourceId?: string | null) => {
11641163
if (!sourceId) return;
1165-
const mgr = sourceManagerRegistry.getManager(sourceId) as { startDistanceDeleteScheduler?: () => Promise<void> } | undefined;
1166-
mgr?.startDistanceDeleteScheduler?.().catch((err: unknown) =>
1164+
const mgr = sourceManagerRegistry.getManager(sourceId);
1165+
mgr?.startDistanceDeleteScheduler().catch((err: unknown) =>
11671166
logger.error(`Failed to restart auto-delete-by-distance scheduler for source ${sourceId}:`, err));
11681167
},
11691168
stopAutoDeleteByDistanceService: (sourceId?: string | null) => {
11701169
if (!sourceId) return;
1171-
const mgr = sourceManagerRegistry.getManager(sourceId) as { stopDistanceDeleteScheduler?: () => void } | undefined;
1172-
mgr?.stopDistanceDeleteScheduler?.();
1170+
const mgr = sourceManagerRegistry.getManager(sourceId);
1171+
mgr?.stopDistanceDeleteScheduler();
11731172
},
11741173
});
11751174

src/server/sourceManagerRegistry.meshcore.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ function makeMeshtasticStub(sourceId = 'mt-1'): ISourceManager {
5151
longName: 'Test Node',
5252
shortName: 'TN',
5353
}),
54+
startDistanceDeleteScheduler: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
55+
stopDistanceDeleteScheduler: vi.fn<() => void>(),
5456
};
5557
}
5658

0 commit comments

Comments
 (0)