Skip to content

Commit

Permalink
deepFreeze object in development
Browse files Browse the repository at this point in the history
  • Loading branch information
nigrosimone committed Aug 21, 2022
1 parent 2efaec1 commit 0e65dfa
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,40 @@ describe('NgSimpleStateBaseStore: Service', () => {
expect(service.getFirstState()).toEqual({ count: 1 });
expect(service.getCurrentState()).toEqual({ count: 1 });
});

it('deepFreeze', () => {

try {
expect(service['devMode']).toEqual(true);
const state = service.getFirstState();
expect(state).toEqual({ count: 1 });
if (state) {
(state as any).count = 2;
}
expect(state).toEqual({ count: 2 });
} catch (error: any) {
expect(error.message).toEqual('!');
}

try {
expect(service['devMode']).toEqual(true);
service['devMode'] = false;
expect(service['devMode']).toEqual(false);
const state = service.getFirstState();
if (state) {
expect(state).toEqual({ count: 2 });
(state as any).count = 3;
}
expect(true).toEqual(false);
} catch (error: any) {
expect(error.message).toEqual('Cannot assign to read only property \'count\' of object \'[object Object]\'');
} finally {
service['devMode'] = true;
expect(service['devMode']).toEqual(true);
}

});

});

@Component({
Expand Down
42 changes: 37 additions & 5 deletions projects/ng-simple-state/src/lib/ng-simple-state-base-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Inject, Injectable, Injector, OnDestroy, Directive } from '@angular/core';
import { Inject, Injectable, Injector, OnDestroy, Directive, isDevMode } from '@angular/core';
import { BehaviorSubject, Observable, asyncScheduler } from 'rxjs';
import { map, distinctUntilChanged, observeOn } from 'rxjs/operators';
import { NgSimpleStateBrowserStorage } from './ng-simple-state-browser-storage';
Expand All @@ -21,6 +21,7 @@ export abstract class NgSimpleStateBaseStore<S extends object | Array<any>> impl
private storeName: string;
private firstState: S | null = null;
private isArray: boolean;
private devMode: boolean = isDevMode();

/**
* Return the observable of the state
Expand Down Expand Up @@ -121,8 +122,8 @@ export abstract class NgSimpleStateBaseStore<S extends object | Array<any>> impl
* Return the current store state (snapshot)
* @returns The current state
*/
getCurrentState(): S {
return this.state$.getValue();
getCurrentState(): Readonly<S> {
return this.deepFreeze(this.state$.getValue());
}

/**
Expand All @@ -131,8 +132,8 @@ export abstract class NgSimpleStateBaseStore<S extends object | Array<any>> impl
* otherwise the initial state provided from `initialState()` method.
* @returns The first state
*/
getFirstState(): S | null {
return this.firstState;
getFirstState(): Readonly<S> | null {
return this.deepFreeze(this.firstState);
}

/**
Expand Down Expand Up @@ -182,4 +183,35 @@ export abstract class NgSimpleStateBaseStore<S extends object | Array<any>> impl
}
return this.devTool.send(this.storeName, actionName || 'unknown', newState);
}

/**
* Recursively Object.freeze simple Javascript structures consisting of plain objects, arrays, and primitives.
* Make the data immutable.
* @returns immutable object
*/
private deepFreeze(object: S | null): Readonly<S> {
// No freezing in production (for better performance).
if (this.devMode || !object) {
return object as Readonly<S>;
}

// When already frozen, we assume its children are frozen (for better performance).
// This should be true if you always use `deepFreeze` to freeze objects,
// which is why you should have a linter rule that prevents you from using
// `Object.freeze` standalone.
//
// Note that Object.isFrozen will also return `true` for primitives (numbers,
// strings, booleans, undefined, null), so there is no need to check for
// those explicitly.
if (Object.isFrozen(object)) {
return object as Readonly<S>;
}

// At this point we know that we're dealing with either an array or plain object, so
// just freeze it and recurse on its values.
Object.freeze(object);
Object.keys(object).forEach(key => this.deepFreeze((object as any)[key]));

return object as Readonly<S>;
}
}

0 comments on commit 0e65dfa

Please sign in to comment.