• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eustatos / nexus-state / 23151573359

16 Mar 2026 03:27PM UTC coverage: 72.999% (-2.4%) from 75.353%
23151573359

push

github

web-flow
feat(form): add features and refactor

2058 of 2422 branches covered (84.97%)

Branch coverage included in aggregate %.

681 of 720 new or added lines in 11 files covered. (94.58%)

19 existing lines in 1 file now uncovered.

7910 of 11233 relevant lines covered (70.42%)

164.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.67
/packages/devtools/src/command-handler.ts
1
/**
1✔
2
 * CommandHandler - Processes DevTools time travel commands
3
 *
4
 * This class handles JUMP_TO_STATE and JUMP_TO_ACTION commands
5
 * from Redux DevTools, integrating with SimpleTimeTravel for
6
 * state navigation.
7
 *
8
 */
9

10
import type {
11
  Command,
12
  JumpToStateCommand,
13
  JumpToActionCommand,
14
  ImportStateCommand,
15
  ImportStateFormat,
16
  CommandHandlerConfig,
17
} from "./types";
18
import type { Snapshot } from "@nexus-state/time-travel";
19
import { SnapshotMapper } from "./snapshot-mapper";
20
import type { SimpleTimeTravel } from "@nexus-state/time-travel";
21
import { StateSerializer, createStateSerializer } from "./state-serializer";
22

23
/**
24
 * CommandHandler class for processing DevTools time travel commands
25
 *
26
 * This class provides type-safe command handling for JUMP_TO_STATE
27
 * and JUMP_TO_ACTION commands from Redux DevTools, with integration
28
 * to SimpleTimeTravel for state navigation.
29
 *
30
 * @class CommandHandler
31
 */
32
export class CommandHandler {
1✔
33
  private timeTravel: SimpleTimeTravel | null = null;
1✔
34
  private snapshotMapper: SnapshotMapper | null = null;
1✔
35
  private config: Required<CommandHandlerConfig>;
36
  private history: Command[] = [];
1✔
37
  private stateSerializer: StateSerializer;
38

39
  /**
40
   * Creates a new CommandHandler instance
41
   * @param config Configuration options for the handler
42
   */
43
  constructor(config: CommandHandlerConfig = {}) {
1✔
44
    this.config = {
41✔
45
      maxHistory: config.maxHistory ?? 50,
41✔
46
      onCommandExecuted: config.onCommandExecuted ?? (() => {}),
41✔
47
      onCommandError: config.onCommandError ?? (() => {}),
41✔
48
      onStateUpdate: config.onStateUpdate ?? (() => {}),
41✔
49
    };
41✔
50
    this.stateSerializer = createStateSerializer();
41✔
51
  }
41✔
52

53
  /**
54
   * Set the SimpleTimeTravel instance for command execution
55
   * @param timeTravel The SimpleTimeTravel instance to use
56
   */
57
  setTimeTravel(timeTravel: SimpleTimeTravel): void {
1✔
58
    this.timeTravel = timeTravel;
22✔
59
  }
22✔
60

61
  /**
62
   * Set the SnapshotMapper instance for action-to-snapshot lookups
63
   * @param mapper The SnapshotMapper instance to use
64
   */
65
  setSnapshotMapper(mapper: SnapshotMapper): void {
1✔
66
    this.snapshotMapper = mapper;
×
67
  }
×
68

69
  /**
70
   * Handle a command from DevTools
71
   * @param command The command to handle
72
   * @returns true if command was executed successfully, false otherwise
73
   */
74
  handleCommand(command: Command): boolean {
1✔
75
    try {
28✔
76
      // Validate command type
77
      if (!command || typeof (command as Command).type !== "string") {
28✔
78
        throw new Error("Invalid command: missing type");
1✔
79
      }
1✔
80

81
      // Validate command payload
82
      if (!(command as Command).payload) {
28✔
83
        throw new Error(
2✔
84
          `Invalid command payload for ${(command as Command).type}`,
2✔
85
        );
2✔
86
      }
2✔
87

88
      // Route to appropriate handler
89
      switch ((command as Command).type) {
25✔
90
        case "JUMP_TO_STATE":
28✔
91
          return this.handleJumpToState(command as JumpToStateCommand);
11✔
92

93
        case "JUMP_TO_ACTION":
28✔
94
          return this.handleJumpToAction(command as JumpToActionCommand);
8✔
95

96
        case "IMPORT_STATE":
28✔
97
          return this.handleImportState(command as ImportStateCommand);
5✔
98

99
        default:
28✔
100
          throw new Error(`Unknown command type: ${(command as Command).type}`);
1✔
101
      }
28✔
102
    } catch (error) {
28✔
103
      this.handleCommandError(command as Command, error as Error);
18✔
104
      return false;
18✔
105
    }
18✔
106
  }
28✔
107

108
  /**
109
   * Handle JUMP_TO_STATE command
110
   * @param command The JUMP_TO_STATE command
111
   * @returns true if successful, false otherwise
112
   */
113
  private handleJumpToState(command: JumpToStateCommand): boolean {
1✔
114
    // Validate index
115
    const { index } = command.payload;
11✔
116
    if (!Number.isInteger(index) || index < 0) {
11✔
117
      throw new Error(`Invalid index: ${index}`);
2✔
118
    }
2✔
119

120
    // Check SimpleTimeTravel integration
121
    if (!this.timeTravel) {
11✔
122
      throw new Error(
1✔
123
        "SimpleTimeTravel not initialized. Call setTimeTravel() first.",
1✔
124
      );
1✔
125
    }
1✔
126

127
    // Get current history length
128
    const history = this.timeTravel.getHistory();
8✔
129
    if (index >= history.length) {
11✔
130
      throw new Error(
2✔
131
        `Index ${index} out of bounds. History length: ${history.length}`,
2✔
132
      );
2✔
133
    }
2✔
134

135
    // Execute jump
136
    const success = this.timeTravel.jumpTo(index);
6✔
137

138
    if (success) {
6✔
139
      this.history.push(command);
6✔
140
      this.config.onCommandExecuted(command, true);
6✔
141
      
142
      // Send updated state to DevTools
143
      this.sendUpdatedState();
6✔
144
      
145
      return true;
6✔
146
    } else {
11!
147
      throw new Error(`Failed to jump to index ${index}`);
×
148
    }
×
149
  }
11✔
150

151
  /**
152
   * Handle JUMP_TO_ACTION command
153
   * @param command The JUMP_TO_ACTION command
154
   * @returns true if successful, false otherwise
155
   */
156
  private handleJumpToAction(command: JumpToActionCommand): boolean {
1✔
157
    const { actionName } = command.payload;
8✔
158

159
    // Validate action name
160
    if (!actionName || typeof actionName !== "string") {
8✔
161
      throw new Error("Invalid action name: must be non-empty string");
2✔
162
    }
2✔
163

164
    // Check SimpleTimeTravel integration
165
    if (!this.timeTravel) {
8✔
166
      throw new Error(
1✔
167
        "SimpleTimeTravel not initialized. Call setTimeTravel() first.",
1✔
168
      );
1✔
169
    }
1✔
170

171
    // Use SnapshotMapper to find snapshot ID for the action
172
    let snapshotId: string | undefined;
5✔
173
    if (this.snapshotMapper) {
8!
174
      snapshotId = this.snapshotMapper.getSnapshotIdByActionId(actionName);
×
175
    }
✔
176

177
    // If no snapshot found via mapper, search history directly
178
    const history = this.timeTravel.getHistory() as { id: string; metadata: { action?: string } }[];
5✔
179
    let foundIndex = -1;
5✔
180

181
    if (snapshotId) {
8!
182
      // Find the index of the snapshot with the matched ID
183
      for (let i = history.length - 1; i >= 0; i--) {
×
184
        if (history[i].id === snapshotId) {
×
185
          foundIndex = i;
×
186
          break;
×
187
        }
×
188
      }
×
189
    }
✔
190

191
    // Fallback: search by action name in metadata if mapper didn't find
192
    if (foundIndex === -1) {
5✔
193
      for (let i = history.length - 1; i >= 0; i--) {
5✔
194
        const snapshot = history[i];
14✔
195
        if (snapshot.metadata.action === actionName) {
14✔
196
          foundIndex = i;
3✔
197
          break;
3✔
198
        }
3✔
199
      }
14✔
200
    }
5✔
201

202
    if (foundIndex === -1) {
8✔
203
      throw new Error(`Action "${actionName}" not found in history`);
2✔
204
    }
2✔
205

206
    // Execute jump
207
    const success = this.timeTravel.jumpTo(foundIndex);
3✔
208

209
    if (success) {
3✔
210
      this.history.push(command);
3✔
211
      this.config.onCommandExecuted(command, true);
3✔
212
      
213
      // Send updated state to DevTools
214
      this.sendUpdatedState();
3✔
215
      
216
      return true;
3✔
217
    } else {
8!
218
      throw new Error(`Failed to jump to action "${actionName}"`);
×
219
    }
×
220
  }
8✔
221

222
  /**
223
   * Handle command execution errors
224
   * @param command The command that failed
225
   * @param error The error that occurred
226
   */
227
  private handleCommandError(command: Command, error: Error): void {
1✔
228
    this.config.onCommandError(command, error);
18✔
229

230
    // Log to console in development
231
    if (process.env.NODE_ENV !== "production") {
18✔
232
      console.warn(
18✔
233
        `[DevTools] Command execution failed:`,
18✔
234
        command.type,
18✔
235
        error.message,
18✔
236
      );
18✔
237
    }
18✔
238
  }
18✔
239

240
  /**
241
   * Get the command execution history
242
   * @returns Array of executed commands
243
   */
244
  getCommandHistory(): Command[] {
1✔
245
    return [...this.history];
2✔
246
  }
2✔
247

248
  /**
249
   * Clear the command history
250
   */
251
  clearCommandHistory(): void {
1✔
252
    this.history = [];
1✔
253
  }
1✔
254

255
  /**
256
   * Handle IMPORT_STATE command
257
   * @param command The IMPORT_STATE command
258
   * @returns true if successful, false otherwise
259
   */
260
  private handleImportState(command: ImportStateCommand): boolean {
1✔
261
    const { payload } = command;
5✔
262

263
    // Validate payload
264
    if (!payload || typeof payload !== "object") {
5!
265
      throw new Error("Invalid import state payload");
×
266
    }
×
267

268
    // Use StateSerializer to validate and deserialize
269
    const result = this.stateSerializer.importState(payload);
5✔
270

271
    if (!result.success) {
5✔
272
      throw new Error(`Failed to import state: ${result.error}`);
4✔
273
    }
4✔
274

275
    // Check SimpleTimeTravel integration
276
    if (!this.timeTravel) {
5!
277
      throw new Error(
×
278
        "SimpleTimeTravel not initialized. Call setTimeTravel() first.",
×
279
      );
×
280
    }
✔
281

282
    // Extract values from SnapshotStateEntry format if needed
283
    const stateToImport: Record<string, unknown> = {};
1✔
284
    for (const [atomIdStr, atomData] of Object.entries(result.state!)) {
1✔
285
      if (atomData && typeof atomData === "object" && "value" in atomData) {
1✔
286
        // This is a SnapshotStateEntry object, extract the value
287
        stateToImport[atomIdStr] = (atomData as any).value;
1✔
288
      } else {
1!
289
        // This is already a plain value
290
        stateToImport[atomIdStr] = atomData;
×
291
      }
×
292
    }
1✔
293

294
    // Import state into SimpleTimeTravel
295
    const success = this.timeTravel.importState(stateToImport);
1✔
296

297
    if (success) {
1✔
298
      this.history.push(command);
1✔
299
      this.config.onCommandExecuted(command, true);
1✔
300
      
301
      // Send updated state to DevTools
302
      this.sendUpdatedState();
1✔
303
      
304
      return true;
1✔
305
    } else {
5!
306
      throw new Error("Failed to import state into SimpleTimeTravel");
×
307
    }
×
308
  }
5✔
309

310
  /**
311
   * Export current state in DevTools-compatible format
312
   * @returns Serialized state with checksum
313
   */
314
  exportState(): Record<string, unknown> | null {
1✔
315
    try {
×
316
      // Check SimpleTimeTravel integration
317
      if (!this.timeTravel) {
×
318
        if (process.env.NODE_ENV !== "production") {
×
319
          console.warn(
×
320
            "SimpleTimeTravel not initialized. Call setTimeTravel() first.",
×
321
          );
×
322
        }
×
323
        return null;
×
324
      }
×
325

326
      // Get current state from SimpleTimeTravel
NEW
327
      const history = this.timeTravel.getHistory() as { id: string; state: Record<string, { value: unknown }>; metadata: { timestamp: number } }[];
×
328
      if (history.length === 0) {
×
329
        return null;
×
330
      }
×
331

332
      const currentSnapshot = history[history.length - 1];
×
333
      const state: Record<string, unknown> = {};
×
334

335
      // Convert snapshot state to plain object
336
      for (const [atomIdStr, atomData] of Object.entries(
×
337
        currentSnapshot.state,
×
338
      )) {
×
339
        state[atomIdStr] = atomData.value;
×
340
      }
×
341

342
      // Use StateSerializer to export with checksum
343
      const exported = this.stateSerializer.exportState(state, {
×
344
        source: "CommandHandler",
×
345
        snapshotId: currentSnapshot.id,
×
346
        timestamp: currentSnapshot.metadata.timestamp,
×
347
      });
×
348

349
      // Convert ExportStateFormat to Record<string, unknown>
350
      return {
×
351
        state: exported.state,
×
352
        timestamp: exported.timestamp,
×
353
        checksum: exported.checksum,
×
354
        version: exported.version,
×
355
        metadata: exported.metadata,
×
356
      };
×
357
    } catch (error) {
×
358
      if (process.env.NODE_ENV !== "production") {
×
359
        console.warn("Failed to export state:", error);
×
360
      }
×
361
      return null;
×
362
    }
×
363
  }
×
364

365
  /**
366
   * Send updated state to DevTools
367
   */
368
  private sendUpdatedState(): void {
1✔
369
    try {
10✔
370
      if (!this.timeTravel) {
10!
371
        return;
×
372
      }
×
373

374
      // Get current state from SimpleTimeTravel
375
      const history = this.timeTravel.getHistory() as { id: string; state: Record<string, { value: unknown }>; metadata: { timestamp: number } }[];
10✔
376
      if (history.length === 0) {
10!
377
        return;
×
378
      }
×
379

380
      const currentSnapshot = history[history.length - 1];
10✔
381
      const state: Record<string, unknown> = {};
10✔
382

383
      // Convert snapshot state to plain object
384
      for (const [atomIdStr, atomData] of Object.entries(currentSnapshot.state)) {
10✔
385
        state[atomIdStr] = atomData.value;
10✔
386
      }
10✔
387

388
      // Call the callback to send state to DevTools
389
      this.config.onStateUpdate(state);
10✔
390
    } catch (error) {
10!
391
      if (process.env.NODE_ENV !== "production") {
×
392
        console.warn("Failed to send updated state to DevTools:", error);
×
393
      }
×
394
    }
×
395
  }
10✔
396
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc