UNPKG

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