• 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

63.02
/packages/core/src/plugins/devtools.ts
1
/**
1✔
2
 * DevTools Plugin — optional Redux DevTools integration
3
 *
4
 * Tree-shakeable plugin that connects to Redux DevTools Extension.
5
 * Only included in bundle when explicitly imported from '@nexus-state/core/debug'.
6
 *
7
 * @packageDocumentation
8
 */
9

10
import type { Store, Plugin, PluginHooks, Atom } from '../types';
11

12
/**
13
 * Options for DevTools plugin
14
 */
15
export interface DevToolsOptions {
16
  /** Display name in DevTools UI */
17
  name?: string;
18
  /** Enable DevTools (default: true in development) */
19
  enabled?: boolean;
20
  /** Maximum history entries for time-travel */
21
  maxHistory?: number;
22
}
23

24
/**
25
 * State update entry for history
26
 */
27
interface HistoryEntry {
28
  state: Record<string, unknown>;
29
  action: string;
30
  timestamp: number;
31
}
32

33
/**
34
 * Redux DevTools Extension connection interface
35
 */
36
interface DevToolsConnection {
37
  init(state: Record<string, unknown>): void;
38
  send(action: Record<string, unknown>, state: Record<string, unknown>): void;
39
  subscribe(callback: (message: unknown) => void): () => void;
40
  disconnect?(): void;
41
}
42

43
/**
44
 * Redux DevTools Extension global
45
 */
46
interface DevToolsExtension {
47
  connect(options: { name: string }): DevToolsConnection;
48
}
49

50
/**
51
 * DevTools plugin factory — connects store to Redux DevTools Extension.
52
 *
53
 * @example
54
 * ```typescript
55
 * import { createStore } from '@nexus-state/core';
56
 * import { devtools } from '@nexus-state/core/debug';
57
 *
58
 * const store = createStore([devtools()]);
59
 * ```
60
 */
61
export function devtools(options?: DevToolsOptions): Plugin {
1✔
62
  const plugin = new DevToolsPluginImpl(options);
6✔
63
  // Return a plugin function that closes the plugin instance
64
  const pluginFn: Plugin = (store: Store): PluginHooks | void => {
6✔
65
    plugin.apply(store);
4✔
66
  };
4✔
67
  return pluginFn;
6✔
68
}
6✔
69

70
/**
71
 * Internal class implementation
72
 */
73
class DevToolsPluginImpl {
1✔
74
  private connection: DevToolsConnection | null = null;
1✔
75
  private history: HistoryEntry[] = [];
1✔
76
  private store: Store | null = null;
1✔
77
  private options: Required<DevToolsOptions>;
78
  private unsubscribe: (() => void) | null = null;
1✔
79

80
  constructor(options?: DevToolsOptions) {
1✔
81
    const isDev =
16✔
82
      typeof process !== 'undefined' &&
16✔
83
      process.env?.NODE_ENV !== 'production';
16✔
84

85
    this.options = {
16✔
86
      name: options?.name ?? 'NexusState',
16✔
87
      enabled: options?.enabled ?? isDev,
16✔
88
      maxHistory: options?.maxHistory ?? 50,
16✔
89
    };
16✔
90
  }
16✔
91

92
  /**
93
   * Apply plugin to store
94
   */
95
  apply(store: Store): void {
1✔
96
    if (!this.options.enabled) {
6✔
97
      return;
4✔
98
    }
4✔
99

100
    this.store = store;
2✔
101
    this.connect();
2✔
102
  }
6✔
103

104
  /**
105
   * Connect to Redux DevTools Extension
106
   */
107
  private connect(): void {
1✔
108
    const win = typeof globalThis !== 'undefined' && typeof (globalThis as Record<string, unknown>).window !== 'undefined'
2!
NEW
109
      ? (globalThis as Record<string, unknown>).window
×
110
      : undefined;
2✔
111

112
    if (win === undefined) {
2✔
113
      return;
2✔
114
    }
2!
115

NEW
116
    const devToolsExt = (win as Record<string, unknown>)[
×
NEW
117
      '__REDUX_DEVTOOLS_EXTENSION__'
×
NEW
118
    ] as DevToolsExtension | undefined;
×
119

NEW
120
    if (devToolsExt === undefined) {
×
NEW
121
      return;
×
NEW
122
    }
×
123

NEW
124
    try {
×
NEW
125
      this.connection = devToolsExt.connect({
×
NEW
126
        name: this.options.name,
×
NEW
127
      });
×
128

129
      // Listen for DevTools commands (time-travel, etc.)
NEW
130
      this.unsubscribe = this.connection.subscribe((message: unknown) => {
×
NEW
131
        const msg = message as { type?: string; payload?: { type?: string; index?: number } };
×
NEW
132
        if (msg.type === 'DISPATCH' && msg.payload !== undefined) {
×
NEW
133
          this.handleDispatch(msg.payload);
×
NEW
134
        }
×
NEW
135
      });
×
136

137
      // Send initial state
NEW
138
      if (this.store !== null) {
×
NEW
139
        this.connection.init(this.store.getState());
×
NEW
140
      }
×
141
    } catch {
2!
142
      // Silently ignore if DevTools extension fails
NEW
143
      this.connection = null;
×
NEW
144
    }
×
145
  }
2✔
146

147
  /**
148
   * Track state change — call this after each atom set
149
   */
150
  trackStateChange(atom: Atom<unknown>, _value: unknown): void {
1✔
151
    if (this.connection === null || this.store === null) {
1!
152
      return;
1✔
153
    }
1!
154

NEW
155
    const state = this.store.getState();
×
NEW
156
    const actionName = 'SET:' + (atom.name ?? atom.id.toString());
×
157

158
    this.connection.send(
1✔
159
      { type: actionName },
1✔
160
      state
1✔
161
    );
1✔
162

163
    // Record history for time-travel
164
    this.history.push({
1✔
165
      state: state,
1✔
166
      action: actionName,
1✔
167
      timestamp: Date.now(),
1✔
168
    });
1✔
169

170
    // Enforce max history
171
    if (this.history.length > this.options.maxHistory) {
1!
NEW
172
      this.history = this.history.slice(this.history.length - this.options.maxHistory);
×
NEW
173
    }
×
174
  }
1✔
175

176
  /**
177
   * Track custom action
178
   */
179
  trackAction(actionName: string, payload: unknown): void {
1✔
180
    if (this.connection === null || this.store === null) {
1!
181
      return;
1✔
182
    }
1!
183

NEW
184
    const state = this.store.getState();
×
185

NEW
186
    this.connection.send(
×
NEW
187
      { type: actionName, payload: payload },
×
NEW
188
      state
×
NEW
189
    );
×
190

NEW
191
    this.history.push({
×
NEW
192
      state: state,
×
NEW
193
      action: actionName,
×
NEW
194
      timestamp: Date.now(),
×
NEW
195
    });
×
196

NEW
197
    if (this.history.length > this.options.maxHistory) {
×
NEW
198
      this.history = this.history.slice(this.history.length - this.options.maxHistory);
×
NEW
199
    }
×
200
  }
1✔
201

202
  /**
203
   * Handle dispatch from DevTools UI
204
   */
205
  private handleDispatch(payload: { type?: string; index?: number }): void {
1✔
NEW
206
    if (this.store === null) {
×
NEW
207
      return;
×
NEW
208
    }
×
209

NEW
210
    if (payload.type === 'JUMP_TO_STATE' && payload.index !== undefined) {
×
NEW
211
      const entry = this.history[payload.index];
×
NEW
212
      if (entry === undefined) {
×
NEW
213
        return;
×
NEW
214
      }
×
215

216
      // Restore state via setState (uses atom names)
NEW
217
      if (this.store.setState !== undefined) {
×
NEW
218
        this.store.setState(entry.state);
×
NEW
219
      }
×
NEW
220
    }
×
221

NEW
222
    if (payload.type === 'RESET') {
×
NEW
223
      this.history = [];
×
NEW
224
    }
×
NEW
225
  }
×
226

227
  /**
228
   * Disconnect from DevTools and clean up
229
   */
230
  disconnect(): void {
1✔
231
    if (this.unsubscribe !== null) {
1!
NEW
232
      this.unsubscribe();
×
NEW
233
      this.unsubscribe = null;
×
NEW
234
    }
×
235

236
    if (this.connection !== null && this.connection.disconnect !== undefined) {
1!
NEW
237
      this.connection.disconnect();
×
NEW
238
    }
×
239

240
    this.connection = null;
1✔
241
    this.history = [];
1✔
242
    this.store = null;
1✔
243
  }
1✔
244

245
  /**
246
   * Get history length
247
   */
248
  getHistoryLength(): number {
1✔
249
    return this.history.length;
3✔
250
  }
3✔
251

252
  /**
253
   * Check if connected
254
   */
255
  isConnected(): boolean {
1✔
256
    return this.connection !== null;
4✔
257
  }
4✔
258
}
1✔
259

260
/**
261
 * DevToolsPlugin class — use `devtools()` factory instead for convenience.
262
 * @deprecated Use `devtools()` factory function
263
 */
264
export class DevToolsPlugin {
1✔
265
  private impl: DevToolsPluginImpl;
266

267
  constructor(options?: DevToolsOptions) {
1✔
268
    this.impl = new DevToolsPluginImpl(options);
10✔
269
  }
10✔
270

271
  /** @deprecated Use `devtools()` factory — returns plugin function */
272
  apply(store: Store): void {
1✔
273
    this.impl.apply(store);
2✔
274
  }
2✔
275

276
  /** Disconnect from DevTools */
277
  disconnect(): void {
1✔
278
    this.impl.disconnect();
1✔
279
  }
1✔
280

281
  /** Get history length */
282
  getHistoryLength(): number {
1✔
283
    return this.impl.getHistoryLength();
3✔
284
  }
3✔
285

286
  /** Check if connected */
287
  isConnected(): boolean {
1✔
288
    return this.impl.isConnected();
4✔
289
  }
4✔
290

291
  /** Track state change */
292
  trackStateChange(atom: Atom<unknown>, value: unknown): void {
1✔
293
    this.impl.trackStateChange(atom, value);
1✔
294
  }
1✔
295

296
  /** Track custom action */
297
  trackAction(actionName: string, payload: unknown): void {
1✔
298
    this.impl.trackAction(actionName, payload);
1✔
299
  }
1✔
300
}
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