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

eustatos / nexus-state / 24570659818

17 Apr 2026 02:35PM UTC coverage: 70.577% (-0.7%) from 71.309%
24570659818

push

github

web-flow
refactor(core): replace global atom-registry with scoped store registry (#78)

* refactor(core): replace global atom-registry with scoped store registry

* version: bump packages for store refactor and ESM-only builds

* refactor: fix build

2193 of 2575 branches covered (85.17%)

Branch coverage included in aggregate %.

467 of 618 new or added lines in 12 files covered. (75.57%)

93 existing lines in 9 files now uncovered.

8323 of 12325 relevant lines covered (67.53%)

3549.76 hits per line

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

14.25
/packages/devtools/src/message-handler.ts
1
/**
1✔
2
 * MessageHandler - Handles messages from DevTools
3
 * 
4
 * This class handles various types of messages from DevTools,
5
 * including time travel commands, import/export state, and
6
 * other DevTools operations.
7
 */
8

9
import type {
10
  DevToolsMessage,
11
  DevToolsConfig,
12
} from "./types";
13
import type { Store } from "@nexus-state/core";
14
import { CommandHandler } from "./command-handler";
15
import { StateSerializer, createStateSerializer } from "./state-serializer";
16
import type { SnapshotMapper } from "./snapshot-mapper";
17

18
/**
19
 * Message handler options
20
 */
21
export interface MessageHandlerOptions {
22
  /** Whether to enable time travel support (default: true) */
23
  enableTimeTravel?: boolean;
24
  /** Whether to enable state import/export (default: true) */
25
  enableImportExport?: boolean;
26
  /** Whether to log message handling (default: false) */
27
  debug?: boolean;
28
  /** Custom handlers for specific message types */
29
  customHandlers?: Record<string, (message: DevToolsMessage, store: Store) => void>;
30
  /** Callback when state is updated after time-travel command */
31
  onStateUpdate?: (state: Record<string, unknown>) => void;
32
}
33

34
/**
35
 * Message handling result
36
 */
37
export interface MessageHandlerResult {
38
  /** Whether message was handled successfully */
39
  success: boolean;
40
  /** Message type */
41
  type: string;
42
  /** Error message if failed */
43
  error?: string;
44
  /** Additional data */
45
  data?: Record<string, unknown>;
46
}
47

48
/**
49
 * MessageHandler class for handling DevTools messages
50
 */
51
export class MessageHandler {
1✔
52
  private options: Required<MessageHandlerOptions>;
53
  private commandHandler: CommandHandler | null = null;
1✔
54
  private stateSerializer: StateSerializer;
55
  private snapshotMapper: SnapshotMapper | null = null;
1✔
56
  private store: Store | null = null;
1✔
57
  private isTracking = true;
1✔
58

59
  constructor(options: MessageHandlerOptions = {}) {
1✔
60
    this.options = {
13✔
61
      enableTimeTravel: options.enableTimeTravel ?? true,
13✔
62
      enableImportExport: options.enableImportExport ?? true,
13✔
63
      debug: options.debug ?? false,
13✔
64
      customHandlers: options.customHandlers ?? {},
13✔
65
      onStateUpdate: options.onStateUpdate ?? (() => {}),
13✔
66
    };
13✔
67

68
    this.stateSerializer = createStateSerializer();
13✔
69

70
    if (this.options.enableTimeTravel) {
13✔
71
      this.commandHandler = new CommandHandler({
13✔
72
        onStateUpdate: (state) => this.onStateUpdate(state),
13✔
73
      });
13✔
74
    }
13✔
75
  }
13✔
76

77
  /**
78
   * Set the store instance
79
   * @param store The store to handle messages for
80
   */
81
  setStore(store: Store): void {
1✔
82
    console.log('[MessageHandler.setStore] Store received:', {
×
83
      hasTimeTravel: 'timeTravel' in store,
×
84
      timeTravelType: typeof (store as any).timeTravel,
×
85
      storeKeys: Object.keys(store).filter(k => !k.startsWith('get') && k !== 'set' && k !== 'subscribe'),
×
86
      storeType: store.constructor?.name || 'unknown'
×
87
    });
×
88
    
89
    this.store = store;
×
90
    
91
    if (this.commandHandler) {
×
92
      // Check if store has time travel capabilities
93
      const storeWithTimeTravel = store as any;
×
94
      if (storeWithTimeTravel.timeTravel && typeof storeWithTimeTravel.timeTravel === "object") {
×
95
        console.log('[MessageHandler] Setting timeTravel on CommandHandler:', !!storeWithTimeTravel.timeTravel);
×
96
        console.log('[MessageHandler] timeTravel object:', {
×
97
          type: typeof storeWithTimeTravel.timeTravel,
×
98
          constructor: storeWithTimeTravel.timeTravel.constructor?.name,
×
99
          hasJumpTo: typeof storeWithTimeTravel.timeTravel.jumpTo === 'function',
×
100
          hasGetHistory: typeof storeWithTimeTravel.timeTravel.getHistory === 'function',
×
101
          hasCapture: typeof storeWithTimeTravel.timeTravel.capture === 'function',
×
102
        });
×
103
        this.commandHandler.setTimeTravel(storeWithTimeTravel.timeTravel);
×
104
      } else {
×
105
        console.log('[MessageHandler] Store does not have timeTravel:', {
×
106
          hasTimeTravel: !!storeWithTimeTravel.timeTravel,
×
107
          type: typeof storeWithTimeTravel.timeTravel,
×
108
          storeKeys: Object.keys(storeWithTimeTravel).filter(k => k !== 'get' && k !== 'set' && k !== 'subscribe')
×
109
        });
×
110
      }
×
111
    }
×
112
  }
×
113

114
  /**
115
   * Set the snapshot mapper
116
   * @param mapper The snapshot mapper instance
117
   */
118
  setSnapshotMapper(mapper: SnapshotMapper): void {
1✔
119
    this.snapshotMapper = mapper;
×
120
    
121
    if (this.commandHandler) {
×
122
      this.commandHandler.setSnapshotMapper(mapper);
×
123
    }
×
124
  }
×
125

126
  /**
127
   * Set the SimpleTimeTravel instance for time travel debugging
128
   * @param timeTravel The SimpleTimeTravel instance
129
   */
130
  setTimeTravel(timeTravel: any): void {
1✔
131
    console.log('[MessageHandler] setTimeTravel called:', {
×
132
      hasJumpTo: typeof timeTravel.jumpTo === 'function',
×
133
      hasGetHistory: typeof timeTravel.getHistory === 'function',
×
134
    });
×
135
    
136
    if (this.commandHandler) {
×
137
      this.commandHandler.setTimeTravel(timeTravel);
×
138
    }
×
139
  }
×
140

141
  /**
142
   * Handle state updates after time-travel commands
143
   */
144
  private onStateUpdate(state: Record<string, unknown>): void {
1✔
145
    console.log('[MessageHandler.onStateUpdate] State updated after time-travel:', {
×
146
      hasState: !!state,
×
147
      stateKeys: Object.keys(state),
×
148
    });
×
149
    
150
    // Call the configured callback if available
151
    this.options.onStateUpdate(state);
×
152
  }
×
153

154
  /**
155
   * Handle a message from DevTools
156
   * @param message The message to handle
157
   * @param store The store to apply commands to
158
   * @returns Message handling result
159
   */
160
  handle(message: DevToolsMessage, store?: Store): MessageHandlerResult {
1✔
161
    console.log('[MessageHandler.handle] Handling message:', message.type);
×
162
    const targetStore = store || this.store;
×
163
    
164
    if (!targetStore) {
×
165
      return {
×
166
        success: false,
×
167
        type: message.type,
×
168
        error: "Store not set. Call setStore() first or provide store parameter.",
×
169
      };
×
170
    }
×
171

172
    try {
×
173
      // Handle different message types
174
      switch (message.type) {
×
175
        case "DISPATCH":
×
176
          console.log('[MessageHandler.handle] Dispatch message detected, calling handleDispatch');
×
177
          return this.handleDispatch(message, targetStore);
×
178

179
        case "ACTION":
×
180
          return this.handleAction(message, targetStore);
×
181

182
        case "START":
×
183
          return this.handleStart(message, targetStore);
×
184

185
        case "STOP":
×
186
          return this.handleStop(message, targetStore);
×
187

188
        default:
×
189
          // Check for custom handlers
190
          if (this.options.customHandlers[message.type]) {
×
191
            return this.handleCustom(message, targetStore);
×
192
          }
×
193
          
194
          return {
×
195
            success: false,
×
196
            type: message.type,
×
197
            error: `Unknown message type: ${message.type}`,
×
198
          };
×
199
      }
×
200
    } catch (error) {
×
201
      if (this.options.debug) {
×
202
        console.error("MessageHandler: Error handling message:", error);
×
203
      }
×
204

205
      return {
×
206
        success: false,
×
207
        type: message.type,
×
208
        error: (error as Error).message || "Unknown error",
×
209
      };
×
210
    }
×
211
  }
×
212

213
  /**
214
   * Handle DISPATCH messages (time travel commands)
215
   */
216
  private handleDispatch(message: DevToolsMessage, store: Store): MessageHandlerResult {
1✔
217
    const payload = message.payload as { type: string; [key: string]: unknown };
×
218
    
219
    if (!payload || typeof payload !== "object") {
×
220
      return {
×
221
        success: false,
×
222
        type: "DISPATCH",
×
223
        error: "Invalid dispatch payload",
×
224
      };
×
225
    }
×
226

227
    switch (payload.type) {
×
228
      case "JUMP_TO_ACTION":
×
229
      case "JUMP_TO_STATE":
×
230
        return this.handleTimeTravel(payload, store);
×
231

232
      case "START":
×
233
        this.isTracking = true;
×
234
        return {
×
235
          success: true,
×
236
          type: "DISPATCH",
×
237
          data: { command: "START" },
×
238
        };
×
239

240
      case "STOP":
×
241
        this.isTracking = false;
×
242
        return {
×
243
          success: true,
×
244
          type: "DISPATCH",
×
245
          data: { command: "STOP" },
×
246
        };
×
247

248
      case "COMMIT":
×
249
        // Commit current state
250
        return {
×
251
          success: true,
×
252
          type: "DISPATCH",
×
253
          data: { command: "COMMIT" },
×
254
        };
×
255

256
      case "RESET":
×
257
        return this.handleReset(payload, store);
×
258

259
      case "IMPORT_STATE":
×
260
        return this.handleImportState(payload, store);
×
261

262
      default:
×
263
        return {
×
264
          success: false,
×
265
          type: "DISPATCH",
×
266
          error: `Unknown dispatch type: ${payload.type}`,
×
267
        };
×
268
    }
×
269
  }
×
270

271
  /**
272
   * Handle time travel commands
273
   */
274
  private handleTimeTravel(
1✔
275
    _payload: { type: string; [key: string]: unknown },
×
NEW
276
    _store: Store,
×
277
  ): MessageHandlerResult {
×
278
    if (!this.options.enableTimeTravel || !this.commandHandler) {
×
279
      return {
×
280
        success: false,
×
281
        type: _payload.type,
×
282
        error: "Time travel is not enabled",
×
283
      };
×
284
    }
×
285

286
    // Convert to Command format expected by CommandHandler
287
    const command = {
×
288
      type: _payload.type,
×
289
      payload: _payload,
×
290
    };
×
291

292
    const success = this.commandHandler.handleCommand(command);
×
293

294
    return {
×
295
      success,
×
296
      type: _payload.type,
×
297
      data: { command: _payload.type },
×
298
    };
×
299
  }
×
300

301
  /**
302
   * Handle reset command
303
   */
304
  private handleReset(
1✔
305
    _payload: { type: string; [key: string]: unknown },
×
NEW
306
    _store: Store,
×
307
  ): MessageHandlerResult {
×
308
    // Reset to initial state would require storing the initial state
309
    if (this.options.debug) {
×
310
      console.warn("Reset is not fully supported without storing initial state");
×
311
    }
×
312

313
    return {
×
314
      success: false,
×
315
      type: "RESET",
×
316
      error: "Reset is not fully supported without storing initial state",
×
317
    };
×
318
  }
×
319

320
  /**
321
   * Handle import state command
322
   */
323
  private handleImportState(
1✔
324
    payload: { type: string; [key: string]: unknown },
×
NEW
325
    store: Store,
×
326
  ): MessageHandlerResult {
×
327
    if (!this.options.enableImportExport) {
×
328
      return {
×
329
        success: false,
×
330
        type: "IMPORT_STATE",
×
331
        error: "Import/export is not enabled",
×
332
      };
×
333
    }
×
334

335
    try {
×
336
      // Extract import data from payload
337
      const importData = payload.state || payload.payload;
×
338

339
      if (!importData) {
×
340
        return {
×
341
          success: false,
×
342
          type: "IMPORT_STATE",
×
343
          error: "No state data provided",
×
344
        };
×
345
      }
×
346

347
      // Use StateSerializer to deserialize and validate
348
      const result = this.stateSerializer.importState(importData);
×
349

350
      if (!result.success) {
×
351
        return {
×
352
          success: false,
×
353
          type: "IMPORT_STATE",
×
354
          error: `Failed to import state: ${result.error}`,
×
355
        };
×
356
      }
×
357

358
      // Import state into store
359
      this.importStateIntoStore(result.state!, store);
×
360

361
      if (this.options.debug) {
×
362
        console.log("IMPORT_STATE: State imported successfully");
×
363
      }
×
364

365
      return {
×
366
        success: true,
×
367
        type: "IMPORT_STATE",
×
368
        data: { imported: true },
×
369
      };
×
370
    } catch (error) {
×
371
      return {
×
372
        success: false,
×
373
        type: "IMPORT_STATE",
×
374
        error: (error as Error).message || "Failed to import state",
×
375
      };
×
376
    }
×
377
  }
×
378

379
  /**
380
   * Import state into store
381
   */
382
  private importStateIntoStore(
1✔
383
    state: Record<string, unknown>,
×
NEW
384
    store: Store,
×
385
  ): void {
×
386
    // Check if store has importState method
387
    if (typeof (store as any).importState === "function") {
×
388
      (store as any).importState(state);
×
389
      return;
×
390
    }
×
391

392
    // Fallback: manually set each atom value
393
    // Note: This requires atom registry integration
394
    for (const [atomIdStr, value] of Object.entries(state)) {
×
395
      try {
×
396
        // This is a simplified implementation - real implementation would need
397
        // to convert string atom IDs back to actual atoms
398
        if (this.options.debug) {
×
399
          console.warn(`IMPORT_STATE: Cannot set atom ${atomIdStr} - registry integration needed`);
×
400
        }
×
401
      } catch (error) {
×
402
        if (this.options.debug) {
×
403
          console.warn(`IMPORT_STATE: Failed to set atom ${atomIdStr}:`, error);
×
404
        }
×
405
      }
×
406
    }
×
407
  }
×
408

409
  /**
410
   * Handle ACTION messages
411
   */
412
  private handleAction(_message: DevToolsMessage, _store: Store): MessageHandlerResult {
1✔
413
    // ACTION messages are typically sent from DevTools to dispatch actions
414
    // This would require integration with the store's action system
415

416
    if (this.options.debug) {
×
417
      console.warn("ACTION messages are not fully supported");
×
418
    }
×
419

420
    return {
×
421
      success: false,
×
422
      type: "ACTION",
×
423
      error: "ACTION messages are not fully supported",
×
424
    };
×
425
  }
×
426

427
  /**
428
   * Handle START messages
429
   */
430
  private handleStart(_message: DevToolsMessage, _store: Store): MessageHandlerResult {
1✔
431
    this.isTracking = true;
×
432
    return {
×
433
      success: true,
×
434
      type: "START",
×
435
      data: { tracking: true },
×
436
    };
×
437
  }
×
438

439
  /**
440
   * Handle STOP messages
441
   */
442
  private handleStop(_message: DevToolsMessage, _store: Store): MessageHandlerResult {
1✔
443
    this.isTracking = false;
×
444
    return {
×
445
      success: true,
×
446
      type: "STOP",
×
447
      data: { tracking: false },
×
448
    };
×
449
  }
×
450

451
  /**
452
   * Handle custom messages
453
   */
454
  private handleCustom(message: DevToolsMessage, store: Store): MessageHandlerResult {
1✔
455
    const handler = this.options.customHandlers[message.type];
×
456

457
    try {
×
458
      handler(message, store);
×
459
      return {
×
460
        success: true,
×
461
        type: message.type,
×
462
        data: { handled: true },
×
463
      };
×
464
    } catch (error) {
×
465
      return {
×
466
        success: false,
×
467
        type: message.type,
×
468
        error: (error as Error).message || "Custom handler error",
×
469
      };
×
470
    }
×
471
  }
×
472

473
  /**
474
   * Jump to a specific state index
475
   * @param index State index to jump to
476
   * @returns True if successful
477
   */
478
  jumpToState(index: number): boolean {
1✔
479
    if (!this.options.enableTimeTravel || !this.commandHandler) {
×
480
      return false;
×
481
    }
×
482

483
    const command = {
×
484
      type: "JUMP_TO_STATE",
×
485
      payload: { index },
×
486
    };
×
487

488
    return this.commandHandler.handleCommand(command);
×
489
  }
×
490

491
  /**
492
   * Import state from external source
493
   * @param stateData State data to import
494
   * @returns True if successful
495
   */
496
  importState(stateData: unknown): boolean {
1✔
497
    if (!this.options.enableImportExport || !this.store) {
×
498
      return false;
×
499
    }
×
500

501
    const message: DevToolsMessage = {
×
502
      type: "DISPATCH",
×
503
      payload: {
×
504
        type: "IMPORT_STATE",
×
505
        state: stateData,
×
506
      },
×
507
    };
×
508

509
    const result = this.handle(message, this.store);
×
510
    return result.success;
×
511
  }
×
512

513
  /**
514
   * Check if tracking is enabled
515
   * @returns True if tracking
516
   */
517
  isTrackingEnabled(): boolean {
1✔
518
    return this.isTracking;
×
519
  }
×
520

521
  /**
522
   * Get the command handler (if time travel is enabled)
523
   * @returns CommandHandler instance or null
524
   */
525
  getCommandHandler(): CommandHandler | null {
1✔
526
    return this.commandHandler;
×
527
  }
×
528

529
  /**
530
   * Update handler options
531
   * @param newOptions New options
532
   */
533
  updateOptions(newOptions: MessageHandlerOptions): void {
1✔
534
    this.options = {
×
535
      ...this.options,
×
536
      ...newOptions,
×
537
    };
×
538

539
    // Handle time travel enable/disable
540
    if (newOptions.enableTimeTravel === false && this.commandHandler) {
×
541
      this.commandHandler = null;
×
542
    } else if (newOptions.enableTimeTravel === true && !this.commandHandler) {
×
543
      this.commandHandler = new CommandHandler({
×
544
        onStateUpdate: (state) => this.onStateUpdate(state),
×
545
      });
×
546
      
547
      // Re-set time travel if store has it
548
      if (this.store && (this.store as any).timeTravel) {
×
549
        this.commandHandler.setTimeTravel((this.store as any).timeTravel);
×
550
      }
×
551
      
552
      // Re-set snapshot mapper if available
553
      if (this.snapshotMapper) {
×
554
        this.commandHandler.setSnapshotMapper(this.snapshotMapper);
×
555
      }
×
556
    }
×
557
  }
×
558

559
  /**
560
   * Get current options
561
   * @returns Current options
562
   */
563
  getOptions(): Required<MessageHandlerOptions> {
1✔
564
    return { ...this.options };
×
565
  }
×
566
}
1✔
567

568
/**
569
 * Create a new MessageHandler instance
570
 * @param options Message handler options
571
 * @returns New MessageHandler instance
572
 */
573
export function createMessageHandler(options: MessageHandlerOptions = {}): MessageHandler {
1✔
574
  return new MessageHandler(options);
13✔
575
}
13✔
576

577
/**
578
 * Default message handler instance for convenience
579
 */
580
export const defaultMessageHandler = createMessageHandler();
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