-
-
Notifications
You must be signed in to change notification settings - Fork 10
Plan Fire and Forget V2
Roborock is deprecating the synchronous request-response pattern. AbstractClient.get() registers a pending promise via PendingResponseTracker, sends a request, and awaits a correlated response (10s timeout). When Roborock drops response support, every get() call will time out.
Goal: Replace get() with query() using OneShotResponseListener (registers on the existing push pipeline, resolves on first matching message). Then delete the tracker infrastructure entirely.
AbstractClient.get(duid, request)
→ responseTracker.waitFor(request, duid) // registers pending promise keyed by messageId/timestamp
→ sendInternal(duid, request)
→ device push → localClient/mqttClient.onMessage()
→ responseBroadcaster.tryResolve(response) // resolves the pending promise
→ responseBroadcaster.onMessage(response) // dispatches to registered listeners
→ pending promise resolves → return result
-
src/roborockCommunication/routing/client.ts—Clientinterface withget<T>() -
src/roborockCommunication/routing/abstractClient.ts—AbstractClientwithget<T>()andresponseTrackerfield -
src/roborockCommunication/routing/clientRouter.ts—ClientRouter.get<T>() -
src/roborockCommunication/routing/listeners/responseBroadcaster.ts— interface withtryResolve() -
src/roborockCommunication/routing/listeners/responseBroadcasterFactory.ts—implements PendingResponseTracker, haswaitFor(),cancelAll() -
src/roborockCommunication/routing/listeners/v1ResponseBroadcaster.ts—tryResolve()→V1PendingResponseTracker -
src/roborockCommunication/routing/listeners/b01ResponseBroadcaster.ts—tryResolve()→B01PendingResponseTracker -
src/roborockCommunication/routing/services/pendingResponseTracker.ts— interface -
src/roborockCommunication/routing/services/v1PendingResponseTracker.ts— V1 message-id correlation -
src/roborockCommunication/routing/services/b01PendingResponseTracker.ts— B01 timestamp+window collection -
src/roborockCommunication/local/localClient.ts:221—responseBroadcaster.tryResolve(response)call -
src/roborockCommunication/mqtt/mqttClient.ts:262—responseBroadcaster.tryResolve(response)call -
src/roborockCommunication/protocol/dispatcher/V10MessageDispatcher.ts— allclient.get()calls -
src/roborockCommunication/protocol/dispatcher/Q10MessageDispatcher.ts—client.get()ingetNetworkInfo,getMapInfo,getRoomMap,getCustomMessage -
src/roborockCommunication/protocol/dispatcher/Q7MessageDispatcher.ts—client.get()ingetMapInfo,getRoomMap,getCustomMessage -
src/roborockCommunication/protocol/dispatcher/abstractMessageDispatcher.ts— interface
src/roborockCommunication/routing/listeners/implementation/helloResponseListener.ts — already the one-shot pattern with waitFor() returning a promise that resolves on first matching message.
Goal: Eliminate all get() callers that never used the response.
// BEFORE
public async findMyRobot(duid: string): Promise<void> {
const request = new RequestMessage({ method: 'find_me' });
await this.client.get(duid, request);
}
// AFTER
public async findMyRobot(duid: string): Promise<void> {
const request = new RequestMessage({ method: 'find_me' });
await this.client.send(duid, request);
}// BEFORE (line 10)
getDeviceStatus(duid: string): Promise<DeviceStatus | undefined>;
// AFTER
getDeviceStatus(duid: string): Promise<void>;// BEFORE
public async getDeviceStatus(duid: string): Promise<DeviceStatus | undefined> {
const request = new RequestMessage({ method: 'get_prop', params: ['get_status'] });
const response = await this.client.get<CloudMessageResult[]>(duid, request);
if (response) {
this.logger.debug('Device status: ', debugStringify(response));
return new DeviceStatus(duid, response[0]);
}
return undefined;
}
// AFTER
public async getDeviceStatus(duid: string): Promise<void> {
const request = new RequestMessage({ method: 'get_prop', params: ['get_status'] });
await this.client.send(duid, request);
}Also remove CloudMessageResult from imports if no longer used.
// BEFORE
public async getNetworkInfo(duid: string): Promise<NetworkInfo | undefined> {
const request = new RequestMessage({ messageId: this.messageId, dps: { [Q10RequestCode.dps_request]: 1 } });
const response = await this.client.get(duid, request);
this.logger.notice(`Get network info: ${debugStringify(response)}`);
return undefined;
}
// AFTER
public async getNetworkInfo(duid: string): Promise<NetworkInfo | undefined> {
const request = new RequestMessage({ messageId: this.messageId, dps: { [Q10RequestCode.dps_request]: 1 } });
await this.client.send(duid, request);
return undefined;
}Remove debugStringify from imports if no longer used.
// BEFORE
public async getDeviceStatus(duid: string): Promise<DeviceStatus | undefined> {
const request = new RequestMessage({ messageId: this.messageId, dps: { [Q10RequestCode.dps_request]: 1 } });
await this.client.get(duid, request);
return undefined;
}
// AFTER
public async getDeviceStatus(duid: string): Promise<void> {
const request = new RequestMessage({ messageId: this.messageId, dps: { [Q10RequestCode.dps_request]: 1 } });
await this.client.send(duid, request);
}// BEFORE
public async getMapInfo(duid: string): Promise<MapInfo> {
const request = new RequestMessage({ ... });
const response = await this.client.get<object>(duid, request);
this.logger.notice(`Get map info response for Q10 device ${duid}: ${response ? debugStringify(response) : 'no response'}`);
return new MapInfo({ max_multi_map: 0, max_bak_map: 0, multi_map_count: 0, map_info: [] });
}
// AFTER
public async getMapInfo(duid: string): Promise<MapInfo> {
const request = new RequestMessage({ ... });
await this.client.send(duid, request);
return new MapInfo({ max_multi_map: 0, max_bak_map: 0, multi_map_count: 0, map_info: [] });
}// BEFORE
public async getRoomMap(duid: string, activeMap: number): Promise<RawRoomMappingData> {
const request = new RequestMessage({ messageId: this.messageId, dps: { [Q10RequestCode.get_prop]: 1 } });
const response = await this.client.get<{ room_mapping: RawRoomMappingData }>(duid, request);
return response?.room_mapping ?? [];
}
// AFTER
public async getRoomMap(duid: string, activeMap: number): Promise<RawRoomMappingData> {
await this.client.send(duid, new RequestMessage({ messageId: this.messageId, dps: { [Q10RequestCode.get_prop]: 1 } }));
return [];
}// BEFORE
public async getMapInfo(duid: string): Promise<MapInfo> {
const request = new RequestMessage({ ... });
const response = await this.client.get<object>(duid, request);
this.logger.notice(`Get map info response for Q7 device ${duid}: ${response ? debugStringify(response) : 'no response'}`);
return new MapInfo({ max_multi_map: 0, max_bak_map: 0, multi_map_count: 0, map_info: [] });
}
// AFTER
public async getMapInfo(duid: string): Promise<MapInfo> {
await this.client.send(duid, new RequestMessage({ messageId: this.messageId, dps: this.createDps(Q7RequestMethod.get_map_list, {}) }));
return new MapInfo({ max_multi_map: 0, max_bak_map: 0, multi_map_count: 0, map_info: [] });
}// BEFORE
public async getRoomMap(duid: string, activeMap: number): Promise<RawRoomMappingData> {
const request = new RequestMessage({ ... });
const response = (await this.client.get<RawRoomMappingData>(duid, request)) ?? [];
this.logger.notice(`Get room map response for Q7 device ${duid}: ...`);
return response;
}
// AFTER
public async getRoomMap(duid: string, activeMap: number): Promise<RawRoomMappingData> {
await this.client.send(duid, new RequestMessage({
messageId: this.messageId,
dps: this.createDps(Q7RequestMethod.get_room_mapping_backup_1, { map_id: activeMap, prefer_type: 1 }),
}));
return [];
}New file: src/roborockCommunication/routing/listeners/oneShotResponseListener.ts
import { MESSAGE_TIMEOUT_MS } from "../../../constants/index.js";
import { ResponseMessage } from "../../models/responseMessage.js";
import { AbstractMessageListener } from "./abstractMessageListener.js";
export class OneShotResponseListener<T> implements AbstractMessageListener {
readonly name = "OneShotResponseListener";
private resolve?: (value: T) => void;
private reject?: (error: Error) => void;
private timer?: NodeJS.Timeout;
private settled = false;
constructor(
public readonly duid: string,
private readonly parseFn: (msg: ResponseMessage) => T | undefined,
private readonly timeoutMs: number = MESSAGE_TIMEOUT_MS,
) {}
public waitFor(): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
this.timer = setTimeout(() => {
if (!this.settled) {
this.settled = true;
reject(
new Error(
`[OneShotResponseListener] Timeout after ${this.timeoutMs}ms for duid: ${this.duid}`,
),
);
}
}, this.timeoutMs);
});
}
public async onMessage(message: ResponseMessage): Promise<void> {
if (this.settled) return;
if (message.duid !== this.duid) return;
const result = this.parseFn(message);
if (result === undefined) return;
this.settled = true;
if (this.timer) {
clearTimeout(this.timer);
this.timer.unref();
}
this.resolve?.(result);
}
}Notes:
- No auto-unregister — after settling,
onMessageis a no-op (dormant in listeners array) - Precedent:
helloResponseListener.tspattern
File: src/roborockCommunication/routing/client.ts
// Add after send():
query<T>(duid: string, request: RequestMessage, parseFn: (msg: ResponseMessage) => T | undefined, timeoutMs?: number): Promise<T | undefined>;Import ResponseMessage from '../models/index.js'.
File: src/roborockCommunication/routing/abstractClient.ts
Add imports:
import { OneShotResponseListener } from "./listeners/oneShotResponseListener.js";
import { ResponseMessage } from "../models/responseMessage.js";Add method after send():
public async query<T>(
duid: string,
request: RequestMessage,
parseFn: (msg: ResponseMessage) => T | undefined,
timeoutMs?: number,
): Promise<T | undefined> {
try {
const listener = new OneShotResponseListener<T>(duid, parseFn, timeoutMs);
this.responseBroadcaster.register(listener);
this.sendInternal(duid, request);
return await listener.waitFor();
} catch (error) {
this.logger.error(error instanceof Error ? error.message : String(error));
return undefined;
}
}File: src/roborockCommunication/routing/clientRouter.ts
Add imports:
import { OneShotResponseListener } from "./listeners/oneShotResponseListener.js";
import { ResponseMessage } from "../models/index.js";Add method after send():
public async query<T>(
duid: string,
request: RequestMessage,
parseFn: (msg: ResponseMessage) => T | undefined,
timeoutMs?: number,
): Promise<T | undefined> {
try {
const listener = new OneShotResponseListener<T>(duid, parseFn, timeoutMs);
this.broadcasterFactory.register(listener);
await this.send(duid, request);
return await listener.waitFor();
} catch (error) {
this.logger.error(error instanceof Error ? error.message : String(error));
return undefined;
}
}Add at module level (before the class), used by all V10 parseFns:
import { DpsPayload, Protocol, ResponseMessage } from "../../models/index.js";
function parseV1Result<T>(
msg: ResponseMessage,
messageId: number,
): T | undefined {
if (!msg.body) return undefined;
let dps = msg.get(Protocol.rpc_response) as DpsPayload;
if (!dps) dps = msg.get(Protocol.general_response) as DpsPayload;
if (!dps) dps = msg.get(Protocol.general_request) as DpsPayload;
if (!dps || dps.id !== messageId) return undefined;
const result = dps.result;
if (!result) return undefined;
return (Array.isArray(result) ? result[0] : result) as T;
}Note: See V1PendingResponseTracker.tryResolve() lines 57-82 for the exact extraction pattern — response.get(Protocol.rpc_response) as DpsPayload.
// BEFORE
public async getNetworkInfo(duid: string): Promise<NetworkInfo | undefined> {
const request = new RequestMessage({ method: 'get_network_info' });
return await this.client.get(duid, request);
}
// AFTER
public async getNetworkInfo(duid: string): Promise<NetworkInfo | undefined> {
const request = new RequestMessage({ method: 'get_network_info' });
return await this.client.query<NetworkInfo>(
duid,
request,
(msg) => parseV1Result<NetworkInfo>(msg, request.messageId),
);
}// BEFORE
public async getSerialNumber(duid: string): Promise<string | undefined> {
const request = new RequestMessage({ method: 'get_serial_number' });
const response = await this.client.get<{ serial_number: string }[]>(duid, request);
return response && response.length > 0 ? response[0].serial_number : duid;
}
// AFTER
public async getSerialNumber(duid: string): Promise<string | undefined> {
const request = new RequestMessage({ method: 'get_serial_number' });
const response = await this.client.query<{ serial_number: string }[]>(
duid,
request,
(msg) => {
// result IS the array — do not take result[0]
const dps = msg.get(Protocol.rpc_response) as DpsPayload ?? msg.get(Protocol.general_response) as DpsPayload;
if (!dps || dps.id !== request.messageId) return undefined;
return dps.result as { serial_number: string }[];
},
);
return response && response.length > 0 ? response[0].serial_number : duid;
}Note: parseV1Result returns result[0] by default. For getSerialNumber the whole array is the result — use inline parseFn returning dps.result directly.
public async getHomeMap(duid: string): Promise<MapRoomResponse> {
const request = new RequestMessage({ method: 'get_map_v1', secure: true });
const response = await this.client.query<MapRoomResponse>(
duid,
request,
(msg) => parseV1Result<MapRoomResponse>(msg, request.messageId),
);
return response ?? {};
}public async getMapInfo(duid: string): Promise<MapInfo> {
const request = new RequestMessage({ method: 'get_multi_maps_list' });
const response = await this.client.query<MultipleMapDto>(
duid,
request,
(msg) => parseV1Result<MultipleMapDto>(msg, request.messageId),
);
return new MapInfo(
response ?? { max_multi_map: 0, max_bak_map: 0, multi_map_count: 0, map_info: [] },
);
}Note: Current code takes response[0] from MultipleMapDto[]. parseV1Result already returns result[0], so the type is MultipleMapDto (not MultipleMapDto[]).
public async getRoomMap(duid: string, activeMap: number): Promise<RawRoomMappingData> {
const request = new RequestMessage({ method: 'get_room_mapping' });
const response = await this.client.query<RawRoomMappingData>(
duid,
request,
(msg) => {
const dps = msg.get(Protocol.rpc_response) as DpsPayload ?? msg.get(Protocol.general_response) as DpsPayload;
if (!dps || dps.id !== request.messageId) return undefined;
return dps.result as RawRoomMappingData;
},
);
return response ?? [];
}// BEFORE
public getCustomMessage<T = unknown>(duid: string, def: RequestMessage): Promise<T> {
return this.client.get(duid, def) as Promise<T>;
}
// AFTER
public async getCustomMessage<T = unknown>(duid: string, def: RequestMessage): Promise<T> {
const result = await this.client.query<T>(
duid,
def,
(msg) => parseV1Result<T>(msg, def.messageId),
);
return result as T;
}Calls this.getCustomMessage() which is already migrated in 2.11. No direct edits required.
// BEFORE (lines 157-169) — reads current mode before writing
const currentMopMode = await this.getCustomMessage<number>(
duid,
new RequestMessage({ method: "get_custom_mode" }),
);
const smartMopMode = VacuumSuctionPower.Smart;
const smartMopRoute = MopRoute.Smart;
const customMopMode = VacuumSuctionPower.Custom;
const customMopRoute = MopRoute.Custom;
if (currentMopMode == smartMopMode && mopRoute == smartMopRoute) return;
if (currentMopMode == customMopMode && mopRoute == customMopRoute) return;
if (currentMopMode == smartMopMode) {
await this.client.send(
duid,
new RequestMessage({ method: "set_mop_mode", params: [customMopRoute] }),
);
}
// AFTER — unconditional exit-smart-mode send (no-op on device if already custom)
const smartMopRoute = MopRoute.Smart;
const customMopRoute = MopRoute.Custom;
if (mopRoute && mopRoute !== smartMopRoute) {
await this.client.send(
duid,
new RequestMessage({ method: "set_mop_mode", params: [customMopRoute] }),
);
}Remove VacuumSuctionPower import if no longer used elsewhere. Keep the rest of changeCleanMode() unchanged.
// BEFORE
public async getCustomMessage<T = unknown>(duid: string, def: RequestMessage): Promise<T> {
const request = new RequestMessage({ ...def, messageId: this.messageId });
return this.client.get(duid, request) as Promise<T>;
}
// AFTER
public async getCustomMessage<T = unknown>(duid: string, def: RequestMessage): Promise<T> {
const request = new RequestMessage({ ...def, messageId: this.messageId });
const result = await this.client.query<T>(duid, request, (msg) => {
if (!msg.body) return undefined;
const data = msg.body.data;
if (!data) return undefined;
const values = Object.values(data);
return values.length > 0 ? (values[0] as T) : undefined;
});
return result as T;
}Note: Verify this parseFn against actual B01 push message structure. See B01PendingResponseTracker.mergeData() / mapDataKeys() for the pushed data shape.
// BEFORE
public async getCustomMessage<T = unknown>(duid: string, def: RequestMessage): Promise<T> {
const request = new RequestMessage({ ...def, messageId: this.messageId });
return this.client.get(duid, request) as Promise<T>;
}
// AFTER
public async getCustomMessage<T = unknown>(duid: string, def: RequestMessage): Promise<T> {
const request = new RequestMessage({ ...def, messageId: this.messageId });
const result = await this.client.query<T>(duid, request, (msg) => {
if (!msg.body) return undefined;
const data = msg.body.data;
if (!data) return undefined;
const values = Object.values(data);
return values.length > 0 ? (values[0] as T) : undefined;
});
return result as T;
}File: src/roborockCommunication/routing/client.ts
- Delete:
get<T>(duid: string, request: RequestMessage): Promise<T | undefined>;
File: src/roborockCommunication/routing/abstractClient.ts
- Delete
private readonly responseTracker: PendingResponseTrackerconstructor param (line 30) - Delete
import { PendingResponseTracker } from './services/pendingResponseTracker.js';(line 13) - Delete
public async get<T>()method (lines 81-92)
File: src/roborockCommunication/routing/clientRouter.ts
- Delete
public async get<T>()method (lines 103-109)
File: src/roborockCommunication/routing/listeners/responseBroadcaster.ts
- Delete:
tryResolve(response: ResponseMessage): void;
File: src/roborockCommunication/routing/listeners/v1ResponseBroadcaster.ts
- Delete
private readonly tracker: V1PendingResponseTrackerconstructor param - Delete
import { V1PendingResponseTracker }import - Delete
public tryResolve()method (lines 27-29) - Update
unregister(): removethis.tracker.cancelAll()— justthis.listeners = []
File: src/roborockCommunication/routing/listeners/b01ResponseBroadcaster.ts
- Delete
private readonly tracker: B01PendingResponseTrackerconstructor param - Delete
import { B01PendingResponseTracker }import - Delete
public waitFor()method (lines 27-29) - Delete
public tryResolve()method (lines 31-33) - Update
unregister(): remove tracker call — justthis.listeners = []
File: src/roborockCommunication/routing/listeners/responseBroadcasterFactory.ts
- Remove
implements PendingResponseTrackerfrom class declaration - Delete
private readonly v1Trackerandprivate readonly b01Trackerfields - Delete imports:
B01PendingResponseTracker,V1PendingResponseTracker,PendingResponseTracker,RequestMessage - Delete methods:
tryResolve(),waitFor(),cancelAll(),getTrackerForDevice() - Update constructor: remove tracker instantiation (lines 26-27), remove tracker args from broadcaster construction (lines 28-29)
- Keep
getBroadcasterForResponse()— still used byonMessage()(line 48)
File: src/roborockCommunication/local/localClient.ts
- Delete line 221:
this.responseBroadcaster.tryResolve(response); - Remove
PendingResponseTrackerimport (line 13) - Remove
responseTracker: PendingResponseTrackerconstructor param (line 34) - Update
super()call to remove tracker arg (line 36)
File: src/roborockCommunication/mqtt/mqttClient.ts
- Delete line 262:
this.responseBroadcaster.tryResolve(response); - Remove
PendingResponseTrackerimport (line 9) - Remove
responseTracker: PendingResponseTrackerconstructor param (line 29) - Update
super()call to remove tracker arg (line 31)
File: src/roborockCommunication/routing/clientRouter.ts
-
MQTTClientconstructor call (line 28): removethis.broadcasterFactoryas 5th arg -
LocalNetworkClientconstructor call (lines 43-49): removethis.broadcasterFactoryas 6th arg
src/roborockCommunication/routing/services/pendingResponseTracker.tssrc/roborockCommunication/routing/services/v1PendingResponseTracker.tssrc/roborockCommunication/routing/services/b01PendingResponseTracker.ts
src/tests/roborockCommunication/routing/services/v1PendingResponseTracker.test.tssrc/tests/roborockCommunication/routing/services/b01PendingResponseTracker.test.ts
File: src/tests/roborockCommunication/routing/listeners/responseBroadcasterFactory.test.ts
Remove these test cases (lines 102-226):
'should route tryResolve to V1 broadcaster for V1 responses''should route tryResolve to B01 broadcaster for B01 responses''should route waitFor to V1 tracker for V1 devices''should route waitFor to B01 tracker for B01 devices''should cancelAll pending requests from B01 tracker''should fall back to V1 tracker when protocol version is not B01'
Remove RequestMessage import if no longer needed.
File: src/tests/roborockCommunication/routing/listeners/v1ResponseBroadcaster.test.ts
- Remove
V1PendingResponseTrackerfrom imports andbeforeEach - Remove any
tryResolvetest cases - Update broadcaster construction:
new V1ResponseBroadcaster(logger)(no tracker param)
File: src/tests/roborockCommunication/routing/listeners/b01ResponseBroadcaster.test.ts
- Remove
B01PendingResponseTrackerfrom imports andbeforeEach - Remove
tryResolve,waitFortest cases - Update broadcaster construction:
new B01ResponseBroadcaster(logger)(no tracker param)
New file: src/tests/roborockCommunication/routing/listeners/oneShotResponseListener.test.ts
Four test cases:
-
resolves when parseFn returns a value — create listener, call
onMessage()with matching msg, assertwaitFor()resolves with parsed value -
ignores messages where parseFn returns undefined — parseFn returns
undefined, assert listener stays pending -
rejects on timeout — use
vi.useFakeTimers(), advance pastMESSAGE_TIMEOUT_MS, assertwaitFor()rejects with timeout error -
is a no-op after settling — resolve once, call
onMessage()again, assert parseFn called only once / no double-resolve
Execute in this sequence — each step must type-check before the next:
-
Phase 1 (1.1–1.9):
get()→send()conversions +getDeviceStatusreturn type -
Phase 2 (2.1): create
OneShotResponseListener -
Phase 2 (2.2–2.4): add
query()toClient,AbstractClient,ClientRouter -
Phase 2 (2.5–2.15): migrate all remaining
get()callers in dispatchers - Phase 3 (3.1–3.11): remove tracker infrastructure
- Phase 3 (3.12–3.16): test file updates
Verify after each phase: npm run type-check
| File | Action | Phase |
|---|---|---|
routing/client.ts |
Add query<T>(), remove get<T>()
|
2+3 |
routing/abstractClient.ts |
Add query(), remove get() + responseTracker
|
2+3 |
routing/clientRouter.ts |
Add query(), remove get<T>(), update client construction |
2+3 |
routing/listeners/oneShotResponseListener.ts |
CREATE | 2 |
routing/listeners/responseBroadcaster.ts |
Remove tryResolve()
|
3 |
routing/listeners/responseBroadcasterFactory.ts |
Remove tracker/waitFor/cancelAll/implements | 3 |
routing/listeners/v1ResponseBroadcaster.ts |
Remove tryResolve(), tracker field |
3 |
routing/listeners/b01ResponseBroadcaster.ts |
Remove tryResolve(), waitFor(), tracker field |
3 |
routing/services/pendingResponseTracker.ts |
DELETE | 3 |
routing/services/v1PendingResponseTracker.ts |
DELETE | 3 |
routing/services/b01PendingResponseTracker.ts |
DELETE | 3 |
local/localClient.ts |
Remove tryResolve call, remove tracker param |
3 |
mqtt/mqttClient.ts |
Remove tryResolve call, remove tracker param |
3 |
protocol/dispatcher/abstractMessageDispatcher.ts |
getDeviceStatus() → Promise<void>
|
1 |
protocol/dispatcher/V10MessageDispatcher.ts |
findMyRobot→send, getDeviceStatus→send, all query() migrations, changeCleanMode fix | 1+2 |
protocol/dispatcher/Q10MessageDispatcher.ts |
getNetworkInfo/getDeviceStatus/getMapInfo/getRoomMap→send, getCustomMessage→query | 1+2 |
protocol/dispatcher/Q7MessageDispatcher.ts |
getMapInfo/getRoomMap→send+[], getCustomMessage→query | 1+2 |
tests/routing/listeners/responseBroadcasterFactory.test.ts |
Remove tracker test cases | 3 |
tests/routing/listeners/v1ResponseBroadcaster.test.ts |
Remove tryResolve tests, update construction | 3 |
tests/routing/listeners/b01ResponseBroadcaster.test.ts |
Remove tryResolve/waitFor tests, update construction | 3 |
tests/routing/services/v1PendingResponseTracker.test.ts |
DELETE | 3 |
tests/routing/services/b01PendingResponseTracker.test.ts |
DELETE | 3 |
tests/routing/listeners/oneShotResponseListener.test.ts |
CREATE (4 test cases) | 3 |
All paths relative to src/roborockCommunication/.
-
parseV1Resultreturn shape: Returnsresult[0]for single-item arrays (standard V1). ForgetSerialNumberthe whole array is the result — use inline parseFn returningdps.resultdirectly. -
ResponseMessage.get(Protocol.xxx): SeeV1PendingResponseTracker.tryResolve()lines 57-82 for the exact extraction pattern. -
ResponseBroadcasterFactory.getBroadcasterForResponse(): Currently called by bothtryResolve()andonMessage(). After removingtryResolve(), keep this method —onMessage()still uses it. -
debugStringifyimport cleanup: Q10 and Q7 dispatchers importdebugStringifyonly for removed log lines. Remove if nothing else uses it. -
B01 parseFn: The Q10/Q7
getCustomMessageparseFn is a best estimate. Verify against actual B01 push message format by inspectingB01PendingResponseTracker.mergeData()/mapDataKeys().