UNPKG

45.1 kBJavaScriptView Raw
1import * as i0 from '@angular/core';
2import { InjectionToken, inject, NgZone, Injectable, Inject, makeEnvironmentProviders, NgModule } from '@angular/core';
3import * as i2 from '@ngrx/store';
4import { ActionsSubject, UPDATE, INIT, INITIAL_STATE, StateObservable, ReducerManagerDispatcher } from '@ngrx/store';
5import { EMPTY, Observable, of, merge, queueScheduler, ReplaySubject } from 'rxjs';
6import { share, filter, map, concatMap, timeout, debounceTime, catchError, take, takeUntil, switchMap, skip, observeOn, withLatestFrom, scan } from 'rxjs/operators';
7import { toSignal } from '@angular/core/rxjs-interop';
8
9const PERFORM_ACTION = 'PERFORM_ACTION';
10const REFRESH = 'REFRESH';
11const RESET = 'RESET';
12const ROLLBACK = 'ROLLBACK';
13const COMMIT = 'COMMIT';
14const SWEEP = 'SWEEP';
15const TOGGLE_ACTION = 'TOGGLE_ACTION';
16const SET_ACTIONS_ACTIVE = 'SET_ACTIONS_ACTIVE';
17const JUMP_TO_STATE = 'JUMP_TO_STATE';
18const JUMP_TO_ACTION = 'JUMP_TO_ACTION';
19const IMPORT_STATE = 'IMPORT_STATE';
20const LOCK_CHANGES = 'LOCK_CHANGES';
21const PAUSE_RECORDING = 'PAUSE_RECORDING';
22class PerformAction {
23 constructor(action, timestamp) {
24 this.action = action;
25 this.timestamp = timestamp;
26 this.type = PERFORM_ACTION;
27 if (typeof action.type === 'undefined') {
28 throw new Error('Actions may not have an undefined "type" property. ' +
29 'Have you misspelled a constant?');
30 }
31 }
32}
33class Refresh {
34 constructor() {
35 this.type = REFRESH;
36 }
37}
38class Reset {
39 constructor(timestamp) {
40 this.timestamp = timestamp;
41 this.type = RESET;
42 }
43}
44class Rollback {
45 constructor(timestamp) {
46 this.timestamp = timestamp;
47 this.type = ROLLBACK;
48 }
49}
50class Commit {
51 constructor(timestamp) {
52 this.timestamp = timestamp;
53 this.type = COMMIT;
54 }
55}
56class Sweep {
57 constructor() {
58 this.type = SWEEP;
59 }
60}
61class ToggleAction {
62 constructor(id) {
63 this.id = id;
64 this.type = TOGGLE_ACTION;
65 }
66}
67class SetActionsActive {
68 constructor(start, end, active = true) {
69 this.start = start;
70 this.end = end;
71 this.active = active;
72 this.type = SET_ACTIONS_ACTIVE;
73 }
74}
75class JumpToState {
76 constructor(index) {
77 this.index = index;
78 this.type = JUMP_TO_STATE;
79 }
80}
81class JumpToAction {
82 constructor(actionId) {
83 this.actionId = actionId;
84 this.type = JUMP_TO_ACTION;
85 }
86}
87class ImportState {
88 constructor(nextLiftedState) {
89 this.nextLiftedState = nextLiftedState;
90 this.type = IMPORT_STATE;
91 }
92}
93class LockChanges {
94 constructor(status) {
95 this.status = status;
96 this.type = LOCK_CHANGES;
97 }
98}
99class PauseRecording {
100 constructor(status) {
101 this.status = status;
102 this.type = PAUSE_RECORDING;
103 }
104}
105
106/**
107 * Chrome extension documentation
108 * @see https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md
109 * Firefox extension documentation
110 * @see https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md
111 */
112class StoreDevtoolsConfig {
113 constructor() {
114 /**
115 * Maximum allowed actions to be stored in the history tree (default: `false`)
116 */
117 this.maxAge = false;
118 }
119}
120const STORE_DEVTOOLS_CONFIG = new InjectionToken('@ngrx/store-devtools Options');
121/**
122 * Used to provide a `StoreDevtoolsConfig` for the store-devtools.
123 */
124const INITIAL_OPTIONS = new InjectionToken('@ngrx/store-devtools Initial Config');
125function noMonitor() {
126 return null;
127}
128const DEFAULT_NAME = 'NgRx Store DevTools';
129function createConfig(optionsInput) {
130 const DEFAULT_OPTIONS = {
131 maxAge: false,
132 monitor: noMonitor,
133 actionSanitizer: undefined,
134 stateSanitizer: undefined,
135 name: DEFAULT_NAME,
136 serialize: false,
137 logOnly: false,
138 autoPause: false,
139 trace: false,
140 traceLimit: 75,
141 // Add all features explicitly. This prevent buggy behavior for
142 // options like "lock" which might otherwise not show up.
143 features: {
144 pause: true, // Start/pause recording of dispatched actions
145 lock: true, // Lock/unlock dispatching actions and side effects
146 persist: true, // Persist states on page reloading
147 export: true, // Export history of actions in a file
148 import: 'custom', // Import history of actions from a file
149 jump: true, // Jump back and forth (time travelling)
150 skip: true, // Skip (cancel) actions
151 reorder: true, // Drag and drop actions in the history list
152 dispatch: true, // Dispatch custom actions or action creators
153 test: true, // Generate tests for the selected actions
154 },
155 connectInZone: false,
156 };
157 const options = typeof optionsInput === 'function' ? optionsInput() : optionsInput;
158 const logOnly = options.logOnly
159 ? { pause: true, export: true, test: true }
160 : false;
161 const features = options.features ||
162 logOnly ||
163 DEFAULT_OPTIONS.features;
164 if (features.import === true) {
165 features.import = 'custom';
166 }
167 const config = Object.assign({}, DEFAULT_OPTIONS, { features }, options);
168 if (config.maxAge && config.maxAge < 2) {
169 throw new Error(`Devtools 'maxAge' cannot be less than 2, got ${config.maxAge}`);
170 }
171 return config;
172}
173
174function difference(first, second) {
175 return first.filter((item) => second.indexOf(item) < 0);
176}
177/**
178 * Provides an app's view into the state of the lifted store.
179 */
180function unliftState(liftedState) {
181 const { computedStates, currentStateIndex } = liftedState;
182 // At start up NgRx dispatches init actions,
183 // When these init actions are being filtered out by the predicate or safe/block list options
184 // we don't have a complete computed states yet.
185 // At this point it could happen that we're out of bounds, when this happens we fall back to the last known state
186 if (currentStateIndex >= computedStates.length) {
187 const { state } = computedStates[computedStates.length - 1];
188 return state;
189 }
190 const { state } = computedStates[currentStateIndex];
191 return state;
192}
193function unliftAction(liftedState) {
194 return liftedState.actionsById[liftedState.nextActionId - 1];
195}
196/**
197 * Lifts an app's action into an action on the lifted store.
198 */
199function liftAction(action) {
200 return new PerformAction(action, +Date.now());
201}
202/**
203 * Sanitizes given actions with given function.
204 */
205function sanitizeActions(actionSanitizer, actions) {
206 return Object.keys(actions).reduce((sanitizedActions, actionIdx) => {
207 const idx = Number(actionIdx);
208 sanitizedActions[idx] = sanitizeAction(actionSanitizer, actions[idx], idx);
209 return sanitizedActions;
210 }, {});
211}
212/**
213 * Sanitizes given action with given function.
214 */
215function sanitizeAction(actionSanitizer, action, actionIdx) {
216 return {
217 ...action,
218 action: actionSanitizer(action.action, actionIdx),
219 };
220}
221/**
222 * Sanitizes given states with given function.
223 */
224function sanitizeStates(stateSanitizer, states) {
225 return states.map((computedState, idx) => ({
226 state: sanitizeState(stateSanitizer, computedState.state, idx),
227 error: computedState.error,
228 }));
229}
230/**
231 * Sanitizes given state with given function.
232 */
233function sanitizeState(stateSanitizer, state, stateIdx) {
234 return stateSanitizer(state, stateIdx);
235}
236/**
237 * Read the config and tell if actions should be filtered
238 */
239function shouldFilterActions(config) {
240 return config.predicate || config.actionsSafelist || config.actionsBlocklist;
241}
242/**
243 * Return a full filtered lifted state
244 */
245function filterLiftedState(liftedState, predicate, safelist, blocklist) {
246 const filteredStagedActionIds = [];
247 const filteredActionsById = {};
248 const filteredComputedStates = [];
249 liftedState.stagedActionIds.forEach((id, idx) => {
250 const liftedAction = liftedState.actionsById[id];
251 if (!liftedAction)
252 return;
253 if (idx &&
254 isActionFiltered(liftedState.computedStates[idx], liftedAction, predicate, safelist, blocklist)) {
255 return;
256 }
257 filteredActionsById[id] = liftedAction;
258 filteredStagedActionIds.push(id);
259 filteredComputedStates.push(liftedState.computedStates[idx]);
260 });
261 return {
262 ...liftedState,
263 stagedActionIds: filteredStagedActionIds,
264 actionsById: filteredActionsById,
265 computedStates: filteredComputedStates,
266 };
267}
268/**
269 * Return true is the action should be ignored
270 */
271function isActionFiltered(state, action, predicate, safelist, blockedlist) {
272 const predicateMatch = predicate && !predicate(state, action.action);
273 const safelistMatch = safelist &&
274 !action.action.type.match(safelist.map((s) => escapeRegExp(s)).join('|'));
275 const blocklistMatch = blockedlist &&
276 action.action.type.match(blockedlist.map((s) => escapeRegExp(s)).join('|'));
277 return predicateMatch || safelistMatch || blocklistMatch;
278}
279/**
280 * Return string with escaped RegExp special characters
281 * https://stackoverflow.com/a/6969486/1337347
282 */
283function escapeRegExp(s) {
284 return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
285}
286
287function injectZoneConfig(connectInZone) {
288 const ngZone = connectInZone ? inject(NgZone) : null;
289 return { ngZone, connectInZone };
290}
291
292class DevtoolsDispatcher extends ActionsSubject {
293 /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: DevtoolsDispatcher, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
294 /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: DevtoolsDispatcher }); }
295}
296i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: DevtoolsDispatcher, decorators: [{
297 type: Injectable
298 }] });
299
300const ExtensionActionTypes = {
301 START: 'START',
302 DISPATCH: 'DISPATCH',
303 STOP: 'STOP',
304 ACTION: 'ACTION',
305};
306const REDUX_DEVTOOLS_EXTENSION = new InjectionToken('@ngrx/store-devtools Redux Devtools Extension');
307class DevtoolsExtension {
308 constructor(devtoolsExtension, config, dispatcher) {
309 this.config = config;
310 this.dispatcher = dispatcher;
311 this.zoneConfig = injectZoneConfig(this.config.connectInZone);
312 this.devtoolsExtension = devtoolsExtension;
313 this.createActionStreams();
314 }
315 notify(action, state) {
316 if (!this.devtoolsExtension) {
317 return;
318 }
319 // Check to see if the action requires a full update of the liftedState.
320 // If it is a simple action generated by the user's app and the recording
321 // is not locked/paused, only send the action and the current state (fast).
322 //
323 // A full liftedState update (slow: serializes the entire liftedState) is
324 // only required when:
325 // a) redux-devtools-extension fires the @@Init action (ignored by
326 // @ngrx/store-devtools)
327 // b) an action is generated by an @ngrx module (e.g. @ngrx/effects/init
328 // or @ngrx/store/update-reducers)
329 // c) the state has been recomputed due to time-traveling
330 // d) any action that is not a PerformAction to err on the side of
331 // caution.
332 if (action.type === PERFORM_ACTION) {
333 if (state.isLocked || state.isPaused) {
334 return;
335 }
336 const currentState = unliftState(state);
337 if (shouldFilterActions(this.config) &&
338 isActionFiltered(currentState, action, this.config.predicate, this.config.actionsSafelist, this.config.actionsBlocklist)) {
339 return;
340 }
341 const sanitizedState = this.config.stateSanitizer
342 ? sanitizeState(this.config.stateSanitizer, currentState, state.currentStateIndex)
343 : currentState;
344 const sanitizedAction = this.config.actionSanitizer
345 ? sanitizeAction(this.config.actionSanitizer, action, state.nextActionId)
346 : action;
347 this.sendToReduxDevtools(() => this.extensionConnection.send(sanitizedAction, sanitizedState));
348 }
349 else {
350 // Requires full state update
351 const sanitizedLiftedState = {
352 ...state,
353 stagedActionIds: state.stagedActionIds,
354 actionsById: this.config.actionSanitizer
355 ? sanitizeActions(this.config.actionSanitizer, state.actionsById)
356 : state.actionsById,
357 computedStates: this.config.stateSanitizer
358 ? sanitizeStates(this.config.stateSanitizer, state.computedStates)
359 : state.computedStates,
360 };
361 this.sendToReduxDevtools(() => this.devtoolsExtension.send(null, sanitizedLiftedState, this.getExtensionConfig(this.config)));
362 }
363 }
364 createChangesObservable() {
365 if (!this.devtoolsExtension) {
366 return EMPTY;
367 }
368 return new Observable((subscriber) => {
369 const connection = this.zoneConfig.connectInZone
370 ? // To reduce change detection cycles, we need to run the `connect` method
371 // outside of the Angular zone. The `connect` method adds a `message`
372 // event listener to communicate with an extension using `window.postMessage`
373 // and handle message events.
374 this.zoneConfig.ngZone.runOutsideAngular(() => this.devtoolsExtension.connect(this.getExtensionConfig(this.config)))
375 : this.devtoolsExtension.connect(this.getExtensionConfig(this.config));
376 this.extensionConnection = connection;
377 connection.init();
378 connection.subscribe((change) => subscriber.next(change));
379 return connection.unsubscribe;
380 });
381 }
382 createActionStreams() {
383 // Listens to all changes
384 const changes$ = this.createChangesObservable().pipe(share());
385 // Listen for the start action
386 const start$ = changes$.pipe(filter((change) => change.type === ExtensionActionTypes.START));
387 // Listen for the stop action
388 const stop$ = changes$.pipe(filter((change) => change.type === ExtensionActionTypes.STOP));
389 // Listen for lifted actions
390 const liftedActions$ = changes$.pipe(filter((change) => change.type === ExtensionActionTypes.DISPATCH), map((change) => this.unwrapAction(change.payload)), concatMap((action) => {
391 if (action.type === IMPORT_STATE) {
392 // State imports may happen in two situations:
393 // 1. Explicitly by user
394 // 2. User activated the "persist state accross reloads" option
395 // and now the state is imported during reload.
396 // Because of option 2, we need to give possible
397 // lazy loaded reducers time to instantiate.
398 // As soon as there is no UPDATE action within 1 second,
399 // it is assumed that all reducers are loaded.
400 return this.dispatcher.pipe(filter((action) => action.type === UPDATE), timeout(1000), debounceTime(1000), map(() => action), catchError(() => of(action)), take(1));
401 }
402 else {
403 return of(action);
404 }
405 }));
406 // Listen for unlifted actions
407 const actions$ = changes$.pipe(filter((change) => change.type === ExtensionActionTypes.ACTION), map((change) => this.unwrapAction(change.payload)));
408 const actionsUntilStop$ = actions$.pipe(takeUntil(stop$));
409 const liftedUntilStop$ = liftedActions$.pipe(takeUntil(stop$));
410 this.start$ = start$.pipe(takeUntil(stop$));
411 // Only take the action sources between the start/stop events
412 this.actions$ = this.start$.pipe(switchMap(() => actionsUntilStop$));
413 this.liftedActions$ = this.start$.pipe(switchMap(() => liftedUntilStop$));
414 }
415 unwrapAction(action) {
416 // indirect eval according to https://esbuild.github.io/content-types/#direct-eval
417 return typeof action === 'string' ? (0, eval)(`(${action})`) : action;
418 }
419 getExtensionConfig(config) {
420 const extensionOptions = {
421 name: config.name,
422 features: config.features,
423 serialize: config.serialize,
424 autoPause: config.autoPause ?? false,
425 trace: config.trace ?? false,
426 traceLimit: config.traceLimit ?? 75,
427 // The action/state sanitizers are not added to the config
428 // because sanitation is done in this class already.
429 // It is done before sending it to the devtools extension for consistency:
430 // - If we call extensionConnection.send(...),
431 // the extension would call the sanitizers.
432 // - If we call devtoolsExtension.send(...) (aka full state update),
433 // the extension would NOT call the sanitizers, so we have to do it ourselves.
434 };
435 if (config.maxAge !== false /* support === 0 */) {
436 extensionOptions.maxAge = config.maxAge;
437 }
438 return extensionOptions;
439 }
440 sendToReduxDevtools(send) {
441 try {
442 send();
443 }
444 catch (err) {
445 console.warn('@ngrx/store-devtools: something went wrong inside the redux devtools', err);
446 }
447 }
448 /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: DevtoolsExtension, deps: [{ token: REDUX_DEVTOOLS_EXTENSION }, { token: STORE_DEVTOOLS_CONFIG }, { token: DevtoolsDispatcher }], target: i0.ɵɵFactoryTarget.Injectable }); }
449 /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: DevtoolsExtension }); }
450}
451i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: DevtoolsExtension, decorators: [{
452 type: Injectable
453 }], ctorParameters: () => [{ type: undefined, decorators: [{
454 type: Inject,
455 args: [REDUX_DEVTOOLS_EXTENSION]
456 }] }, { type: StoreDevtoolsConfig, decorators: [{
457 type: Inject,
458 args: [STORE_DEVTOOLS_CONFIG]
459 }] }, { type: DevtoolsDispatcher }] });
460
461const INIT_ACTION = { type: INIT };
462const RECOMPUTE = '@ngrx/store-devtools/recompute';
463const RECOMPUTE_ACTION = { type: RECOMPUTE };
464/**
465 * Computes the next entry in the log by applying an action.
466 */
467function computeNextEntry(reducer, action, state, error, errorHandler) {
468 if (error) {
469 return {
470 state,
471 error: 'Interrupted by an error up the chain',
472 };
473 }
474 let nextState = state;
475 let nextError;
476 try {
477 nextState = reducer(state, action);
478 }
479 catch (err) {
480 nextError = err.toString();
481 errorHandler.handleError(err);
482 }
483 return {
484 state: nextState,
485 error: nextError,
486 };
487}
488/**
489 * Runs the reducer on invalidated actions to get a fresh computation log.
490 */
491function recomputeStates(computedStates, minInvalidatedStateIndex, reducer, committedState, actionsById, stagedActionIds, skippedActionIds, errorHandler, isPaused) {
492 // Optimization: exit early and return the same reference
493 // if we know nothing could have changed.
494 if (minInvalidatedStateIndex >= computedStates.length &&
495 computedStates.length === stagedActionIds.length) {
496 return computedStates;
497 }
498 const nextComputedStates = computedStates.slice(0, minInvalidatedStateIndex);
499 // If the recording is paused, recompute all states up until the pause state,
500 // else recompute all states.
501 const lastIncludedActionId = stagedActionIds.length - (isPaused ? 1 : 0);
502 for (let i = minInvalidatedStateIndex; i < lastIncludedActionId; i++) {
503 const actionId = stagedActionIds[i];
504 const action = actionsById[actionId].action;
505 const previousEntry = nextComputedStates[i - 1];
506 const previousState = previousEntry ? previousEntry.state : committedState;
507 const previousError = previousEntry ? previousEntry.error : undefined;
508 const shouldSkip = skippedActionIds.indexOf(actionId) > -1;
509 const entry = shouldSkip
510 ? previousEntry
511 : computeNextEntry(reducer, action, previousState, previousError, errorHandler);
512 nextComputedStates.push(entry);
513 }
514 // If the recording is paused, the last state will not be recomputed,
515 // because it's essentially not part of the state history.
516 if (isPaused) {
517 nextComputedStates.push(computedStates[computedStates.length - 1]);
518 }
519 return nextComputedStates;
520}
521function liftInitialState(initialCommittedState, monitorReducer) {
522 return {
523 monitorState: monitorReducer(undefined, {}),
524 nextActionId: 1,
525 actionsById: { 0: liftAction(INIT_ACTION) },
526 stagedActionIds: [0],
527 skippedActionIds: [],
528 committedState: initialCommittedState,
529 currentStateIndex: 0,
530 computedStates: [],
531 isLocked: false,
532 isPaused: false,
533 };
534}
535/**
536 * Creates a history state reducer from an app's reducer.
537 */
538function liftReducerWith(initialCommittedState, initialLiftedState, errorHandler, monitorReducer, options = {}) {
539 /**
540 * Manages how the history actions modify the history state.
541 */
542 return (reducer) => (liftedState, liftedAction) => {
543 let { monitorState, actionsById, nextActionId, stagedActionIds, skippedActionIds, committedState, currentStateIndex, computedStates, isLocked, isPaused, } = liftedState || initialLiftedState;
544 if (!liftedState) {
545 // Prevent mutating initialLiftedState
546 actionsById = Object.create(actionsById);
547 }
548 function commitExcessActions(n) {
549 // Auto-commits n-number of excess actions.
550 let excess = n;
551 let idsToDelete = stagedActionIds.slice(1, excess + 1);
552 for (let i = 0; i < idsToDelete.length; i++) {
553 if (computedStates[i + 1].error) {
554 // Stop if error is found. Commit actions up to error.
555 excess = i;
556 idsToDelete = stagedActionIds.slice(1, excess + 1);
557 break;
558 }
559 else {
560 delete actionsById[idsToDelete[i]];
561 }
562 }
563 skippedActionIds = skippedActionIds.filter((id) => idsToDelete.indexOf(id) === -1);
564 stagedActionIds = [0, ...stagedActionIds.slice(excess + 1)];
565 committedState = computedStates[excess].state;
566 computedStates = computedStates.slice(excess);
567 currentStateIndex =
568 currentStateIndex > excess ? currentStateIndex - excess : 0;
569 }
570 function commitChanges() {
571 // Consider the last committed state the new starting point.
572 // Squash any staged actions into a single committed state.
573 actionsById = { 0: liftAction(INIT_ACTION) };
574 nextActionId = 1;
575 stagedActionIds = [0];
576 skippedActionIds = [];
577 committedState = computedStates[currentStateIndex].state;
578 currentStateIndex = 0;
579 computedStates = [];
580 }
581 // By default, aggressively recompute every state whatever happens.
582 // This has O(n) performance, so we'll override this to a sensible
583 // value whenever we feel like we don't have to recompute the states.
584 let minInvalidatedStateIndex = 0;
585 switch (liftedAction.type) {
586 case LOCK_CHANGES: {
587 isLocked = liftedAction.status;
588 minInvalidatedStateIndex = Infinity;
589 break;
590 }
591 case PAUSE_RECORDING: {
592 isPaused = liftedAction.status;
593 if (isPaused) {
594 // Add a pause action to signal the devtools-user the recording is paused.
595 // The corresponding state will be overwritten on each update to always contain
596 // the latest state (see Actions.PERFORM_ACTION).
597 stagedActionIds = [...stagedActionIds, nextActionId];
598 actionsById[nextActionId] = new PerformAction({
599 type: '@ngrx/devtools/pause',
600 }, +Date.now());
601 nextActionId++;
602 minInvalidatedStateIndex = stagedActionIds.length - 1;
603 computedStates = computedStates.concat(computedStates[computedStates.length - 1]);
604 if (currentStateIndex === stagedActionIds.length - 2) {
605 currentStateIndex++;
606 }
607 minInvalidatedStateIndex = Infinity;
608 }
609 else {
610 commitChanges();
611 }
612 break;
613 }
614 case RESET: {
615 // Get back to the state the store was created with.
616 actionsById = { 0: liftAction(INIT_ACTION) };
617 nextActionId = 1;
618 stagedActionIds = [0];
619 skippedActionIds = [];
620 committedState = initialCommittedState;
621 currentStateIndex = 0;
622 computedStates = [];
623 break;
624 }
625 case COMMIT: {
626 commitChanges();
627 break;
628 }
629 case ROLLBACK: {
630 // Forget about any staged actions.
631 // Start again from the last committed state.
632 actionsById = { 0: liftAction(INIT_ACTION) };
633 nextActionId = 1;
634 stagedActionIds = [0];
635 skippedActionIds = [];
636 currentStateIndex = 0;
637 computedStates = [];
638 break;
639 }
640 case TOGGLE_ACTION: {
641 // Toggle whether an action with given ID is skipped.
642 // Being skipped means it is a no-op during the computation.
643 const { id: actionId } = liftedAction;
644 const index = skippedActionIds.indexOf(actionId);
645 if (index === -1) {
646 skippedActionIds = [actionId, ...skippedActionIds];
647 }
648 else {
649 skippedActionIds = skippedActionIds.filter((id) => id !== actionId);
650 }
651 // Optimization: we know history before this action hasn't changed
652 minInvalidatedStateIndex = stagedActionIds.indexOf(actionId);
653 break;
654 }
655 case SET_ACTIONS_ACTIVE: {
656 // Toggle whether an action with given ID is skipped.
657 // Being skipped means it is a no-op during the computation.
658 const { start, end, active } = liftedAction;
659 const actionIds = [];
660 for (let i = start; i < end; i++)
661 actionIds.push(i);
662 if (active) {
663 skippedActionIds = difference(skippedActionIds, actionIds);
664 }
665 else {
666 skippedActionIds = [...skippedActionIds, ...actionIds];
667 }
668 // Optimization: we know history before this action hasn't changed
669 minInvalidatedStateIndex = stagedActionIds.indexOf(start);
670 break;
671 }
672 case JUMP_TO_STATE: {
673 // Without recomputing anything, move the pointer that tell us
674 // which state is considered the current one. Useful for sliders.
675 currentStateIndex = liftedAction.index;
676 // Optimization: we know the history has not changed.
677 minInvalidatedStateIndex = Infinity;
678 break;
679 }
680 case JUMP_TO_ACTION: {
681 // Jumps to a corresponding state to a specific action.
682 // Useful when filtering actions.
683 const index = stagedActionIds.indexOf(liftedAction.actionId);
684 if (index !== -1)
685 currentStateIndex = index;
686 minInvalidatedStateIndex = Infinity;
687 break;
688 }
689 case SWEEP: {
690 // Forget any actions that are currently being skipped.
691 stagedActionIds = difference(stagedActionIds, skippedActionIds);
692 skippedActionIds = [];
693 currentStateIndex = Math.min(currentStateIndex, stagedActionIds.length - 1);
694 break;
695 }
696 case PERFORM_ACTION: {
697 // Ignore action and return state as is if recording is locked
698 if (isLocked) {
699 return liftedState || initialLiftedState;
700 }
701 if (isPaused ||
702 (liftedState &&
703 isActionFiltered(liftedState.computedStates[currentStateIndex], liftedAction, options.predicate, options.actionsSafelist, options.actionsBlocklist))) {
704 // If recording is paused or if the action should be ignored, overwrite the last state
705 // (corresponds to the pause action) and keep everything else as is.
706 // This way, the app gets the new current state while the devtools
707 // do not record another action.
708 const lastState = computedStates[computedStates.length - 1];
709 computedStates = [
710 ...computedStates.slice(0, -1),
711 computeNextEntry(reducer, liftedAction.action, lastState.state, lastState.error, errorHandler),
712 ];
713 minInvalidatedStateIndex = Infinity;
714 break;
715 }
716 // Auto-commit as new actions come in.
717 if (options.maxAge && stagedActionIds.length === options.maxAge) {
718 commitExcessActions(1);
719 }
720 if (currentStateIndex === stagedActionIds.length - 1) {
721 currentStateIndex++;
722 }
723 const actionId = nextActionId++;
724 // Mutation! This is the hottest path, and we optimize on purpose.
725 // It is safe because we set a new key in a cache dictionary.
726 actionsById[actionId] = liftedAction;
727 stagedActionIds = [...stagedActionIds, actionId];
728 // Optimization: we know that only the new action needs computing.
729 minInvalidatedStateIndex = stagedActionIds.length - 1;
730 break;
731 }
732 case IMPORT_STATE: {
733 // Completely replace everything.
734 ({
735 monitorState,
736 actionsById,
737 nextActionId,
738 stagedActionIds,
739 skippedActionIds,
740 committedState,
741 currentStateIndex,
742 computedStates,
743 isLocked,
744 isPaused,
745 } = liftedAction.nextLiftedState);
746 break;
747 }
748 case INIT: {
749 // Always recompute states on hot reload and init.
750 minInvalidatedStateIndex = 0;
751 if (options.maxAge && stagedActionIds.length > options.maxAge) {
752 // States must be recomputed before committing excess.
753 computedStates = recomputeStates(computedStates, minInvalidatedStateIndex, reducer, committedState, actionsById, stagedActionIds, skippedActionIds, errorHandler, isPaused);
754 commitExcessActions(stagedActionIds.length - options.maxAge);
755 // Avoid double computation.
756 minInvalidatedStateIndex = Infinity;
757 }
758 break;
759 }
760 case UPDATE: {
761 const stateHasErrors = computedStates.filter((state) => state.error).length > 0;
762 if (stateHasErrors) {
763 // Recompute all states
764 minInvalidatedStateIndex = 0;
765 if (options.maxAge && stagedActionIds.length > options.maxAge) {
766 // States must be recomputed before committing excess.
767 computedStates = recomputeStates(computedStates, minInvalidatedStateIndex, reducer, committedState, actionsById, stagedActionIds, skippedActionIds, errorHandler, isPaused);
768 commitExcessActions(stagedActionIds.length - options.maxAge);
769 // Avoid double computation.
770 minInvalidatedStateIndex = Infinity;
771 }
772 }
773 else {
774 // If not paused/locked, add a new action to signal devtools-user
775 // that there was a reducer update.
776 if (!isPaused && !isLocked) {
777 if (currentStateIndex === stagedActionIds.length - 1) {
778 currentStateIndex++;
779 }
780 // Add a new action to only recompute state
781 const actionId = nextActionId++;
782 actionsById[actionId] = new PerformAction(liftedAction, +Date.now());
783 stagedActionIds = [...stagedActionIds, actionId];
784 minInvalidatedStateIndex = stagedActionIds.length - 1;
785 computedStates = recomputeStates(computedStates, minInvalidatedStateIndex, reducer, committedState, actionsById, stagedActionIds, skippedActionIds, errorHandler, isPaused);
786 }
787 // Recompute state history with latest reducer and update action
788 computedStates = computedStates.map((cmp) => ({
789 ...cmp,
790 state: reducer(cmp.state, RECOMPUTE_ACTION),
791 }));
792 currentStateIndex = stagedActionIds.length - 1;
793 if (options.maxAge && stagedActionIds.length > options.maxAge) {
794 commitExcessActions(stagedActionIds.length - options.maxAge);
795 }
796 // Avoid double computation.
797 minInvalidatedStateIndex = Infinity;
798 }
799 break;
800 }
801 default: {
802 // If the action is not recognized, it's a monitor action.
803 // Optimization: a monitor action can't change history.
804 minInvalidatedStateIndex = Infinity;
805 break;
806 }
807 }
808 computedStates = recomputeStates(computedStates, minInvalidatedStateIndex, reducer, committedState, actionsById, stagedActionIds, skippedActionIds, errorHandler, isPaused);
809 monitorState = monitorReducer(monitorState, liftedAction);
810 return {
811 monitorState,
812 actionsById,
813 nextActionId,
814 stagedActionIds,
815 skippedActionIds,
816 committedState,
817 currentStateIndex,
818 computedStates,
819 isLocked,
820 isPaused,
821 };
822 };
823}
824
825class StoreDevtools {
826 constructor(dispatcher, actions$, reducers$, extension, scannedActions, errorHandler, initialState, config) {
827 const liftedInitialState = liftInitialState(initialState, config.monitor);
828 const liftReducer = liftReducerWith(initialState, liftedInitialState, errorHandler, config.monitor, config);
829 const liftedAction$ = merge(merge(actions$.asObservable().pipe(skip(1)), extension.actions$).pipe(map(liftAction)), dispatcher, extension.liftedActions$).pipe(observeOn(queueScheduler));
830 const liftedReducer$ = reducers$.pipe(map(liftReducer));
831 const zoneConfig = injectZoneConfig(config.connectInZone);
832 const liftedStateSubject = new ReplaySubject(1);
833 this.liftedStateSubscription = liftedAction$
834 .pipe(withLatestFrom(liftedReducer$),
835 // The extension would post messages back outside of the Angular zone
836 // because we call `connect()` wrapped with `runOutsideAngular`. We run change
837 // detection only once at the end after all the required asynchronous tasks have
838 // been processed (for instance, `setInterval` scheduled by the `timeout` operator).
839 // We have to re-enter the Angular zone before the `scan` since it runs the reducer
840 // which must be run within the Angular zone.
841 emitInZone(zoneConfig), scan(({ state: liftedState }, [action, reducer]) => {
842 let reducedLiftedState = reducer(liftedState, action);
843 // On full state update
844 // If we have actions filters, we must filter completely our lifted state to be sync with the extension
845 if (action.type !== PERFORM_ACTION && shouldFilterActions(config)) {
846 reducedLiftedState = filterLiftedState(reducedLiftedState, config.predicate, config.actionsSafelist, config.actionsBlocklist);
847 }
848 // Extension should be sent the sanitized lifted state
849 extension.notify(action, reducedLiftedState);
850 return { state: reducedLiftedState, action };
851 }, { state: liftedInitialState, action: null }))
852 .subscribe(({ state, action }) => {
853 liftedStateSubject.next(state);
854 if (action.type === PERFORM_ACTION) {
855 const unliftedAction = action.action;
856 scannedActions.next(unliftedAction);
857 }
858 });
859 this.extensionStartSubscription = extension.start$
860 .pipe(emitInZone(zoneConfig))
861 .subscribe(() => {
862 this.refresh();
863 });
864 const liftedState$ = liftedStateSubject.asObservable();
865 const state$ = liftedState$.pipe(map(unliftState));
866 Object.defineProperty(state$, 'state', {
867 value: toSignal(state$, { manualCleanup: true, requireSync: true }),
868 });
869 this.dispatcher = dispatcher;
870 this.liftedState = liftedState$;
871 this.state = state$;
872 }
873 ngOnDestroy() {
874 // Even though the store devtools plugin is recommended to be
875 // used only in development mode, it can still cause a memory leak
876 // in microfrontend applications that are being created and destroyed
877 // multiple times during development. This results in excessive memory
878 // consumption, as it prevents entire apps from being garbage collected.
879 this.liftedStateSubscription.unsubscribe();
880 this.extensionStartSubscription.unsubscribe();
881 }
882 dispatch(action) {
883 this.dispatcher.next(action);
884 }
885 next(action) {
886 this.dispatcher.next(action);
887 }
888 error(error) { }
889 complete() { }
890 performAction(action) {
891 this.dispatch(new PerformAction(action, +Date.now()));
892 }
893 refresh() {
894 this.dispatch(new Refresh());
895 }
896 reset() {
897 this.dispatch(new Reset(+Date.now()));
898 }
899 rollback() {
900 this.dispatch(new Rollback(+Date.now()));
901 }
902 commit() {
903 this.dispatch(new Commit(+Date.now()));
904 }
905 sweep() {
906 this.dispatch(new Sweep());
907 }
908 toggleAction(id) {
909 this.dispatch(new ToggleAction(id));
910 }
911 jumpToAction(actionId) {
912 this.dispatch(new JumpToAction(actionId));
913 }
914 jumpToState(index) {
915 this.dispatch(new JumpToState(index));
916 }
917 importState(nextLiftedState) {
918 this.dispatch(new ImportState(nextLiftedState));
919 }
920 lockChanges(status) {
921 this.dispatch(new LockChanges(status));
922 }
923 pauseRecording(status) {
924 this.dispatch(new PauseRecording(status));
925 }
926 /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: StoreDevtools, deps: [{ token: DevtoolsDispatcher }, { token: i2.ActionsSubject }, { token: i2.ReducerObservable }, { token: DevtoolsExtension }, { token: i2.ScannedActionsSubject }, { token: i0.ErrorHandler }, { token: INITIAL_STATE }, { token: STORE_DEVTOOLS_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); }
927 /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: StoreDevtools }); }
928}
929i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: StoreDevtools, decorators: [{
930 type: Injectable
931 }], ctorParameters: () => [{ type: DevtoolsDispatcher }, { type: i2.ActionsSubject }, { type: i2.ReducerObservable }, { type: DevtoolsExtension }, { type: i2.ScannedActionsSubject }, { type: i0.ErrorHandler }, { type: undefined, decorators: [{
932 type: Inject,
933 args: [INITIAL_STATE]
934 }] }, { type: StoreDevtoolsConfig, decorators: [{
935 type: Inject,
936 args: [STORE_DEVTOOLS_CONFIG]
937 }] }] });
938/**
939 * If the devtools extension is connected out of the Angular zone,
940 * this operator will emit all events within the zone.
941 */
942function emitInZone({ ngZone, connectInZone, }) {
943 return (source) => connectInZone
944 ? new Observable((subscriber) => source.subscribe({
945 next: (value) => ngZone.run(() => subscriber.next(value)),
946 error: (error) => ngZone.run(() => subscriber.error(error)),
947 complete: () => ngZone.run(() => subscriber.complete()),
948 }))
949 : source;
950}
951
952const IS_EXTENSION_OR_MONITOR_PRESENT = new InjectionToken('@ngrx/store-devtools Is Devtools Extension or Monitor Present');
953function createIsExtensionOrMonitorPresent(extension, config) {
954 return Boolean(extension) || config.monitor !== noMonitor;
955}
956function createReduxDevtoolsExtension() {
957 const extensionKey = '__REDUX_DEVTOOLS_EXTENSION__';
958 if (typeof window === 'object' &&
959 typeof window[extensionKey] !== 'undefined') {
960 return window[extensionKey];
961 }
962 else {
963 return null;
964 }
965}
966/**
967 * Provides developer tools and instrumentation for `Store`.
968 *
969 * @usageNotes
970 *
971 * ```ts
972 * bootstrapApplication(AppComponent, {
973 * providers: [
974 * provideStoreDevtools({
975 * maxAge: 25,
976 * logOnly: !isDevMode(),
977 * }),
978 * ],
979 * });
980 * ```
981 */
982function provideStoreDevtools(options = {}) {
983 return makeEnvironmentProviders([
984 DevtoolsExtension,
985 DevtoolsDispatcher,
986 StoreDevtools,
987 {
988 provide: INITIAL_OPTIONS,
989 useValue: options,
990 },
991 {
992 provide: IS_EXTENSION_OR_MONITOR_PRESENT,
993 deps: [REDUX_DEVTOOLS_EXTENSION, STORE_DEVTOOLS_CONFIG],
994 useFactory: createIsExtensionOrMonitorPresent,
995 },
996 {
997 provide: REDUX_DEVTOOLS_EXTENSION,
998 useFactory: createReduxDevtoolsExtension,
999 },
1000 {
1001 provide: STORE_DEVTOOLS_CONFIG,
1002 deps: [INITIAL_OPTIONS],
1003 useFactory: createConfig,
1004 },
1005 {
1006 provide: StateObservable,
1007 deps: [StoreDevtools],
1008 useFactory: createStateObservable,
1009 },
1010 {
1011 provide: ReducerManagerDispatcher,
1012 useExisting: DevtoolsDispatcher,
1013 },
1014 ]);
1015}
1016
1017function createStateObservable(devtools) {
1018 return devtools.state;
1019}
1020class StoreDevtoolsModule {
1021 static instrument(options = {}) {
1022 return {
1023 ngModule: StoreDevtoolsModule,
1024 providers: [provideStoreDevtools(options)],
1025 };
1026 }
1027 /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: StoreDevtoolsModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1028 /** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.0", ngImport: i0, type: StoreDevtoolsModule }); }
1029 /** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: StoreDevtoolsModule }); }
1030}
1031i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: StoreDevtoolsModule, decorators: [{
1032 type: NgModule,
1033 args: [{}]
1034 }] });
1035
1036/**
1037 * DO NOT EDIT
1038 *
1039 * This file is automatically generated at build
1040 */
1041
1042/**
1043 * Generated bundle index. Do not edit.
1044 */
1045
1046export { INITIAL_OPTIONS, RECOMPUTE, REDUX_DEVTOOLS_EXTENSION, StoreDevtools, StoreDevtoolsConfig, StoreDevtoolsModule, provideStoreDevtools };
1047//# sourceMappingURL=ngrx-store-devtools.mjs.map