From 2be6a455011dacf53a9d9a2d57a9307d3df5c8e1 Mon Sep 17 00:00:00 2001 From: Gabriel Rousseau-Filion Date: Tue, 7 Feb 2017 16:49:04 -0500 Subject: [PATCH 1/4] feat: prevent timing out one tab or window if another tab have activity dd a LocalStorageExpiry that put the expiry value in LocalStorage. If localStorage is not supported by the browser, will store the expiry value in memory. --- .gitignore | 3 + README.md | 4 +- modules/core/index.ts | 5 +- modules/core/src/alternativestorage.spec.ts | 42 +++++++++++ modules/core/src/alternativestorage.ts | 67 +++++++++++++++++ modules/core/src/localstorage.spec.ts | 82 +++++++++++++++++++++ modules/core/src/localstorage.ts | 71 ++++++++++++++++++ modules/core/src/localstorageexpiry.spec.ts | 45 +++++++++++ modules/core/src/localstorageexpiry.ts | 42 +++++++++++ modules/core/src/module.ts | 9 ++- modules/core/src/storageinterruptsource.ts | 22 ++++++ package.json | 4 +- 12 files changed, 387 insertions(+), 9 deletions(-) create mode 100644 modules/core/src/alternativestorage.spec.ts create mode 100644 modules/core/src/alternativestorage.ts create mode 100644 modules/core/src/localstorage.spec.ts create mode 100644 modules/core/src/localstorage.ts create mode 100644 modules/core/src/localstorageexpiry.spec.ts create mode 100644 modules/core/src/localstorageexpiry.ts create mode 100644 modules/core/src/storageinterruptsource.ts diff --git a/.gitignore b/.gitignore index 9a4a5d5..0fd6d7f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ typings # Build artifacts dist .tmp + +# VS Code config +.vscode diff --git a/README.md b/README.md index 8fbb892..78c85bc 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,7 @@ An interrupt is any source of input (typically from the user, but could be thing ### Extensible Expiry Another feature ported from `ng-idle` is the ability to store an expiry value in some store where multiple tabs or windows running the same application can write to. Commonly, this store is the `localStorage`, but could be cookies or whatever you want. The purpose of this expiry and the expiry store is twofold: First, to prevent a window from not timing out if it sleeps or pauses longer than the configured timeout period. Second, it can be used so that activity in one tab or window prevents other tabs or windows in the same application from timing out. -By default, a `SimpleExpiry` type is provided, which will just keep track of the expiry in memory. It will fulfill the first purpose mentioned above, but it will not fulfill the second. In other words, `SimpleExpiry` does not coordinate last activity between tabs or windows; you'll need to use or create an implementation that supports that. An official implementation of that using `localStorage` is forthcoming. You can create your own by extending `IdleExpiry` or `SimpleExpiry` and configuring it as a provider for the `IdleExpiry` class. - -**NOTE** An `IdleExpiry` implementation must be configured. If you don't care about or need this functionality, just use the default `SimpleExpiry` (this is included in `IDLE_PROVIDERS`). +By default, a `LocalStorageExpiry` type is provided, which will just keep track of the expiry in the localStorage. It will fulfill all purposes mentioned above. If you don't want to support multiple tabs or windows, you can use `SimpleExpiry`. In other words, `SimpleExpiry` does not coordinate last activity between tabs or windows. If you want to store the expiry value in another store, like cookies, you'll need to use or create an implementation that supports that. You can create your own by extending `IdleExpiry` or `SimpleExpiry` and configuring it as a provider for the `IdleExpiry` class. ### Multiple Idle Instance Support The dependency injector in Angular 2 supports a hierarchical injection strategy. This allows you to create an instance of `Idle` at whatever scope you need, and there can be more than one instance. This allows you two have two separate watches, for example, on two different elements on the page. diff --git a/modules/core/index.ts b/modules/core/index.ts index 945a475..acdeb05 100644 --- a/modules/core/index.ts +++ b/modules/core/index.ts @@ -1,4 +1,5 @@ import {DocumentInterruptSource} from './src/documentinterruptsource'; +import {StorageInterruptSource} from './src/storageinterruptsource'; export * from './src/idle'; export * from './src/interruptargs'; @@ -6,11 +7,13 @@ export * from './src/interruptsource'; export * from './src/eventtargetinterruptsource'; export * from './src/documentinterruptsource'; export * from './src/windowinterruptsource'; +export * from './src/storageinterruptsource'; export * from './src/keepalivesvc'; export * from './src/idleexpiry'; export * from './src/simpleexpiry'; +export * from './src/localstorageexpiry'; export const DEFAULT_INTERRUPTSOURCES: any[] = [new DocumentInterruptSource( - 'mousemove keydown DOMMouseScroll mousewheel mousedown touchstart touchmove scroll')]; + 'mousemove keydown DOMMouseScroll mousewheel mousedown touchstart touchmove scroll'), new StorageInterruptSource()]; export {NgIdleModule} from './src/module'; diff --git a/modules/core/src/alternativestorage.spec.ts b/modules/core/src/alternativestorage.spec.ts new file mode 100644 index 0000000..9a6988b --- /dev/null +++ b/modules/core/src/alternativestorage.spec.ts @@ -0,0 +1,42 @@ +import { AlternativeStorage } from './alternativestorage'; + +describe('core/AlternativeStorage', () => { + + let storage: Storage; + + beforeEach(() => { + storage = new AlternativeStorage(); + }); + + it('setItem() and getItem() should works properly', () => { + expect(storage.getItem('key')).toBeNull(); + storage.setItem('key', 'value'); + expect(storage.getItem('key')).toBe('value'); + }); + + it('length() returns current value', () => { + expect(storage.length).toBe(0); + storage.setItem('key', 'value'); + expect(storage.length).toBe(1); + }); + + it('clear() must clear current storage', () => { + storage.setItem('key', 'value'); + expect(storage.length).toBe(1); + storage.clear(); + expect(storage.length).toBe(0); + }); + + it('key() must return key name ', () => { + expect(storage.key(0)).toBeNull(); + storage.setItem('key', 'value'); + expect(storage.key(0)).toBe('key'); + }); + + it('remove() must remove item', () => { + storage.setItem('key', 'value'); + storage.removeItem('key'); + expect(storage.getItem('key')).toBeNull(); + }); + +}); diff --git a/modules/core/src/alternativestorage.ts b/modules/core/src/alternativestorage.ts new file mode 100644 index 0000000..018dc4c --- /dev/null +++ b/modules/core/src/alternativestorage.ts @@ -0,0 +1,67 @@ +/* + * Represents an alternative storage for browser that doesn't support localstorage. (i.e. Safari in private mode) + * @implements Storage + */ +export class AlternativeStorage implements Storage { + private storageMap: any = {}; + + /* + * Returns an integer representing the number of data items stored in the storageMap object. + */ + get length() { + return Object.keys(this.storageMap).length; + }; + + /* + * Remove all keys out of the storage. + */ + clear(): void { + this.storageMap = {}; + } + + /* + * Return the key's value + * + * @param key - name of the key to retrieve the value of. + * @return The key's value + */ + getItem(key: string): string | null { + if (typeof this.storageMap[key] !== 'undefined' ) { + return this.storageMap[key]; + } + return null; + } + + /* + * Return the nth key in the storage + * + * @param index - the number of the key you want to get the name of. + * @return The name of the key. + */ + key(index: number): string | null { + return Object.keys(this.storageMap)[index] || null; + } + + /* + * Remove a key from the storage. + * + * @param key - the name of the key you want to remove. + */ + removeItem(key: string): void { + this.storageMap[key] = undefined; + }; + + /* + * Add a key to the storage, or update a key's value if it already exists. + * + * @param key - the name of the key. + * @param value - the value you want to give to the key. + */ + setItem(key: string, value: string): void { + this.storageMap[key] = value; + }; + + [key: string]: any; + [index: number]: string; + +} diff --git a/modules/core/src/localstorage.spec.ts b/modules/core/src/localstorage.spec.ts new file mode 100644 index 0000000..ddf93be --- /dev/null +++ b/modules/core/src/localstorage.spec.ts @@ -0,0 +1,82 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { LocalStorage } from './localstorage'; +import { AlternativeStorage } from './alternativestorage'; + +let mockLocalStorage = {}; + +describe('core/LocalStorage', () => { + + describe('with no localStorage available', () => { + + beforeEach(() => { + TestBed.configureTestingModule(testBedConfiguration()); + initSpyOnLocalStorage(true); + }); + + it('should use AlternativeStorage when no localStorage1', inject([LocalStorage], (service: LocalStorage) => { + expect(service._wrapped() instanceof AlternativeStorage).toBeTruthy(); + })); + + it('setItem() and getItem() should works properly', inject([LocalStorage], (service: LocalStorage) => { + expect(service.getItem('key')).toBeNull(); + service.setItem('key', 'value'); + expect(service.getItem('key')).toBe('value'); + })); + + it('remove() must remove item', inject([LocalStorage], (service: LocalStorage) => { + service.setItem('key', 'value'); + service.removeItem('key'); + expect(service.getItem('key')).toBeNull(); + })); + + }); + + describe('with localStorage available', () => { + + beforeEach(() => { + TestBed.configureTestingModule(testBedConfiguration()); + initSpyOnLocalStorage(false); + }); + + it('should use localStorage if exists', inject([LocalStorage], (service: LocalStorage) => { + expect(service._wrapped() instanceof AlternativeStorage).toBeFalsy(); + })); + + it('setItem() and getItem() should works properly', inject([LocalStorage], (service: LocalStorage) => { + expect(service.getItem('key')).toBeNull(); + service.setItem('key', 'value'); + expect(service.getItem('key')).toBe('value'); + })); + + it('remove() must remove item', inject([LocalStorage], (service: LocalStorage) => { + service.setItem('key', 'value'); + service.removeItem('key'); + expect(service.getItem('key')).toBeNull(); + })); + }); + +}); + +function testBedConfiguration() { + return { + providers: [ + LocalStorage + ] + }; +} + +function initSpyOnLocalStorage(fakeNoLocalStorage: boolean) { + spyOn(localStorage, 'getItem').and.callFake((key: string): String => { + return mockLocalStorage[key] || null; + }); + spyOn(localStorage, 'removeItem').and.callFake((key: string): void => { + delete mockLocalStorage[key]; + }); + spyOn(localStorage, 'setItem').and.callFake((key: string, value: string): string => { + if (fakeNoLocalStorage) { + throw new Error('QuotaExceededError'); + } else { + return mockLocalStorage[key] = value; + } + }); +} diff --git a/modules/core/src/localstorage.ts b/modules/core/src/localstorage.ts new file mode 100644 index 0000000..23704e8 --- /dev/null +++ b/modules/core/src/localstorage.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { AlternativeStorage } from './alternativestorage'; + +/* + * Represents a localStorage store. + */ +@Injectable() +export class LocalStorage { + + private storage: Storage; + + constructor() { + this.storage = this.getStorage(); + } + + /* + * Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem + * throw QuotaExceededError. We're going to detect this and just silently drop any calls to setItem + * to avoid the entire page breaking, without having to do a check at each usage of Storage. + */ + private getStorage(): Storage { + try { + let storage = localStorage; + storage.setItem('ng2IdleStorage', ''); + storage.removeItem('ng2IdleStorage'); + return storage; + } catch (err) { + return new AlternativeStorage(); + } + } + + /* + * Gets an item in the storage. + * + * @param value - The value to get. + * @return The current value. + */ + getItem(key: string): string | null { + return this.storage.getItem('ng2Idle.' + key); + } + + /* + * Removes an item in the storage. + * + * @param value - The value to remove. + */ + removeItem(key: string): void { + this.storage.removeItem('ng2Idle.' + key); + }; + + /* + * Sets an item in the storage. + * + * @param key - The key to set the value. + * @param value - The value to set to the key. + */ + setItem(key: string, data: string): void { + this.storage.setItem('ng2Idle.' + key, data); + }; + + /* + * Represents the storage, commonly use for testing purposes. + * + * @param key - The key to set the value. + * @param value - The value to set to the key. + */ + _wrapped(): Storage { + return this.storage; + } + +} diff --git a/modules/core/src/localstorageexpiry.spec.ts b/modules/core/src/localstorageexpiry.spec.ts new file mode 100644 index 0000000..7b86612 --- /dev/null +++ b/modules/core/src/localstorageexpiry.spec.ts @@ -0,0 +1,45 @@ +import { TestBed, inject } from '@angular/core/testing'; +import {LocalStorageExpiry} from './localstorageexpiry'; +import {LocalStorage} from './localstorage'; + +let mockLocalStorage = {}; + +describe('core/LocalStorageExpiry', () => { + + beforeEach(() => { + TestBed.configureTestingModule(testBedConfiguration()); + initSpyOnLocalStorage(); + }); + + it('last() returns the current value', inject([LocalStorageExpiry], (service: LocalStorageExpiry) => { + expect(service.last()).toBeNull(); + })); + + it('last() sets the specified value', inject([LocalStorageExpiry], (service: LocalStorageExpiry) => { + let expected = new Date(); + expect(service.last(expected)).toEqual(expected); + expect(service.last()).toEqual(expected); + })); + +}); + +function testBedConfiguration() { + return { + providers: [ + LocalStorage, + LocalStorageExpiry + ] + }; +} + +function initSpyOnLocalStorage() { + spyOn(localStorage, 'getItem').and.callFake((key: string): String => { + return mockLocalStorage[key] || null; + }); + spyOn(localStorage, 'removeItem').and.callFake((key: string): void => { + delete mockLocalStorage[key]; + }); + spyOn(localStorage, 'setItem').and.callFake((key: string, value: string): string => { + return mockLocalStorage[key] = value; + }); +} diff --git a/modules/core/src/localstorageexpiry.ts b/modules/core/src/localstorageexpiry.ts new file mode 100644 index 0000000..e5ab873 --- /dev/null +++ b/modules/core/src/localstorageexpiry.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; +import { IdleExpiry } from './idleexpiry'; +import { LocalStorage } from './localstorage'; + +/* + * Represents a localStorage store of expiry values. + * @extends IdleExpiry + */ +@Injectable() +export class LocalStorageExpiry extends IdleExpiry { + + constructor(private localStorage: LocalStorage) { + super(); + } + + /* + * Gets or sets the last expiry date in localStorage. + * If localStorage doesn't work correctly (i.e. Safari in private mode), we store the expiry value in memory. + * @param value - The expiry value to set; omit to only return the value. + * @return The current expiry value. + */ + last(value?: Date): Date { + if (value !== void 0) { + this.setExpiry(value); + } + return this.getExpiry(); + } + + private getExpiry(): Date { + let expiry: string = this.localStorage.getItem('expiry'); + if (expiry) { + return new Date(parseInt(expiry, 10)); + } else { + return null; + } + } + + private setExpiry(value: Date) { + this.localStorage.setItem('expiry', value.getTime().toString()); + } + +} diff --git a/modules/core/src/module.ts b/modules/core/src/module.ts index ae9fdbc..9b5322c 100644 --- a/modules/core/src/module.ts +++ b/modules/core/src/module.ts @@ -2,14 +2,17 @@ import {ModuleWithProviders, NgModule} from '@angular/core'; import {Idle} from './idle'; import {IdleExpiry} from './idleexpiry'; -import {SimpleExpiry} from './simpleexpiry'; +import {LocalStorageExpiry} from './localstorageexpiry'; +import {LocalStorage} from './localstorage'; -@NgModule() +@NgModule({ + providers: [LocalStorage] +}) export class NgIdleModule { static forRoot(): ModuleWithProviders { return { ngModule: NgIdleModule, - providers: [SimpleExpiry, {provide: IdleExpiry, useExisting: SimpleExpiry}, Idle] + providers: [LocalStorageExpiry, {provide: IdleExpiry, useExisting: LocalStorageExpiry}, Idle] }; } } diff --git a/modules/core/src/storageinterruptsource.ts b/modules/core/src/storageinterruptsource.ts new file mode 100644 index 0000000..e99fab9 --- /dev/null +++ b/modules/core/src/storageinterruptsource.ts @@ -0,0 +1,22 @@ +import { WindowInterruptSource } from './windowinterruptsource'; + +/* + * An interrupt source on the storage event of Window. + */ +export class StorageInterruptSource extends WindowInterruptSource { + constructor(throttleDelay = 500) { + super('storage', throttleDelay); + } + + /* + * Checks to see if the event should be filtered. + * @param event - The original event object. + * @return True if the event should be filtered (don't cause an interrupt); otherwise, false. + */ + filterEvent(event: any): boolean { + if (event.key === 'ng2Idle.expiry') { + return false; + } + return true; + } +} diff --git a/package.json b/package.json index 0a5a75f..76488f3 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@types/node": "^6.0.46", "@types/source-map": "^0.1.29", "@types/webpack": "^1.12.35", - "awesome-typescript-loader": "^2.2.1", + "awesome-typescript-loader": "^3.0.3", "conventional-changelog-cli": "^1.2.0", "core-js": "^2.4.1", "coveralls": "^2.11.15", @@ -68,7 +68,7 @@ "ts-helpers": "^1.1.2", "tslint": "^3.15.0", "tslint-loader": "^2.1.5", - "typescript": "^2.0.9", + "typescript": "2.1.5", "webpack": "2.1.0-beta.25", "zone.js": "^0.6.25" }, From 50d30c4290df52a13af7d9f545310376a92e0d51 Mon Sep 17 00:00:00 2001 From: Gabriel Rousseau-Filion Date: Tue, 7 Feb 2017 17:27:24 -0500 Subject: [PATCH 2/4] test: Fix tests --- .../core/src/storageinterruptsource.spec.ts | 40 +++++++++++++++++++ modules/core/src/storageinterruptsource.ts | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 modules/core/src/storageinterruptsource.spec.ts diff --git a/modules/core/src/storageinterruptsource.spec.ts b/modules/core/src/storageinterruptsource.spec.ts new file mode 100644 index 0000000..919d878 --- /dev/null +++ b/modules/core/src/storageinterruptsource.spec.ts @@ -0,0 +1,40 @@ +import {fakeAsync} from '@angular/core/testing'; + +import {StorageInterruptSource} from './storageinterruptsource'; + +describe('core/StorageInterruptSource', () => { + + it('emits onInterrupt event when attached and event is fired', fakeAsync(() => { + let source = new StorageInterruptSource(); + spyOn(source.onInterrupt, 'emit').and.callThrough(); + source.attach(); + + let init: StorageEventInit = { + key: 'ng2Idle.expiry', + oldValue: null, + newValue: '', + url: 'http://localhost:4200/' + }; + + let expected = new StorageEvent('storage', init); + + window.dispatchEvent(expected); + + expect(source.onInterrupt.emit).toHaveBeenCalledTimes(1); + + source.detach(); + })); + + it('does not emit onInterrupt event when detached and event is fired', fakeAsync(() => { + let source = new StorageInterruptSource(); + spyOn(source.onInterrupt, 'emit').and.callThrough(); + // make it interesting by attaching and detaching + source.attach(); + source.detach(); + + let expected = new StorageEvent('storage'); + window.dispatchEvent(expected); + + expect(source.onInterrupt.emit).not.toHaveBeenCalled(); + })); +}); diff --git a/modules/core/src/storageinterruptsource.ts b/modules/core/src/storageinterruptsource.ts index e99fab9..ff55f7e 100644 --- a/modules/core/src/storageinterruptsource.ts +++ b/modules/core/src/storageinterruptsource.ts @@ -13,7 +13,7 @@ export class StorageInterruptSource extends WindowInterruptSource { * @param event - The original event object. * @return True if the event should be filtered (don't cause an interrupt); otherwise, false. */ - filterEvent(event: any): boolean { + filterEvent(event: StorageEvent): boolean { if (event.key === 'ng2Idle.expiry') { return false; } From 1f8b29c6d3bf54e106c8054cf54bb58fc3f65508 Mon Sep 17 00:00:00 2001 From: Gabriel Rousseau-Filion Date: Tue, 7 Feb 2017 17:36:31 -0500 Subject: [PATCH 3/4] fix: fix test again --- .../core/src/storageinterruptsource.spec.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/modules/core/src/storageinterruptsource.spec.ts b/modules/core/src/storageinterruptsource.spec.ts index 919d878..253dde3 100644 --- a/modules/core/src/storageinterruptsource.spec.ts +++ b/modules/core/src/storageinterruptsource.spec.ts @@ -37,4 +37,25 @@ describe('core/StorageInterruptSource', () => { expect(source.onInterrupt.emit).not.toHaveBeenCalled(); })); + + it('does not emit onInterrupt event when attached and key is not ng2Idle.expiry and event is fired', fakeAsync(() => { + let source = new StorageInterruptSource(); + spyOn(source.onInterrupt, 'emit').and.callThrough(); + source.attach(); + + let init: StorageEventInit = { + key: 'ng2Idle.otherKey', + oldValue: null, + newValue: '', + url: 'http://localhost:4200/' + }; + + let expected = new StorageEvent('storage', init); + + window.dispatchEvent(expected); + + expect(source.onInterrupt.emit).not.toHaveBeenCalled(); + + source.detach(); + })); }); From cfd0b1535f4831cb866f26e2f12f4d8aba5b4c42 Mon Sep 17 00:00:00 2001 From: Gabriel Rousseau-Filion Date: Wed, 8 Feb 2017 13:57:31 -0500 Subject: [PATCH 4/4] feat: Can change expiry key name in localstorage for multiple instance --- modules/core/index.ts | 1 + modules/core/src/idle.spec.ts | 491 +++++++++--------- modules/core/src/idle.ts | 13 + modules/core/src/idleexpiry.spec.ts | 4 + modules/core/src/localstorageexpiry.spec.ts | 18 + modules/core/src/localstorageexpiry.ts | 26 +- .../core/src/storageinterruptsource.spec.ts | 6 +- modules/core/src/storageinterruptsource.ts | 2 +- 8 files changed, 320 insertions(+), 241 deletions(-) diff --git a/modules/core/index.ts b/modules/core/index.ts index acdeb05..33f5b14 100644 --- a/modules/core/index.ts +++ b/modules/core/index.ts @@ -11,6 +11,7 @@ export * from './src/storageinterruptsource'; export * from './src/keepalivesvc'; export * from './src/idleexpiry'; export * from './src/simpleexpiry'; +export * from './src/localstorage'; export * from './src/localstorageexpiry'; export const DEFAULT_INTERRUPTSOURCES: any[] = [new DocumentInterruptSource( diff --git a/modules/core/src/idle.spec.ts b/modules/core/src/idle.spec.ts index acaab92..62944c7 100644 --- a/modules/core/src/idle.spec.ts +++ b/modules/core/src/idle.spec.ts @@ -1,19 +1,34 @@ -import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing'; +import { fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; -import {MockExpiry} from '../testing/mockexpiry'; -import {MockInterruptSource} from '../testing/mockinterruptsource'; -import {MockKeepaliveSvc} from '../testing/mockkeepalivesvc'; +import { MockExpiry } from '../testing/mockexpiry'; +import { MockInterruptSource } from '../testing/mockinterruptsource'; +import { MockKeepaliveSvc } from '../testing/mockkeepalivesvc'; -import {AutoResume, Idle} from './idle'; -import {IdleExpiry} from './idleexpiry'; -import {KeepaliveSvc} from './keepalivesvc'; +import { AutoResume, Idle } from './idle'; +import { LocalStorageExpiry } from './localstorageexpiry'; +import { LocalStorage } from './localstorage'; +import { IdleExpiry } from './idleexpiry'; +import { KeepaliveSvc } from './keepalivesvc'; describe('core/Idle', () => { + describe('with LocalStorageExpiry', () => { + + beforeEach(() => { + TestBed.configureTestingModule( + { providers: [LocalStorageExpiry, LocalStorage, { provide: IdleExpiry, useExisting: LocalStorageExpiry }, Idle] }); + }); + + it('setExpiryKey() should set expiry key name', inject([Idle, LocalStorageExpiry], (idle: Idle, exp: LocalStorageExpiry) => { + idle.setExpiryKey('newKeyName'); + expect((exp as LocalStorageExpiry).getExpiryKey()).toBe('newKeyName'); + })); + }); + describe('without KeepaliveSvc integration', () => { beforeEach(() => { TestBed.configureTestingModule( - {providers: [MockExpiry, {provide: IdleExpiry, useExisting: MockExpiry}, Idle]}); + { providers: [MockExpiry, { provide: IdleExpiry, useExisting: MockExpiry }, Idle] }); }); let instance: Idle; @@ -33,8 +48,8 @@ describe('core/Idle', () => { expect(() => { instance.setKeepaliveEnabled(true); }) - .toThrowError( - 'Cannot enable keepalive integration because no KeepaliveSvc has been provided.'); + .toThrowError( + 'Cannot enable keepalive integration because no KeepaliveSvc has been provided.'); }); it('getIdle() should return current value', () => { @@ -48,6 +63,14 @@ describe('core/Idle', () => { expect(actual).toEqual(expected); }); + it('setExpiryKey() when expiry is not instance of LocalStorageExpiry should throw error', () => { + expect(() => { + instance.setExpiryKey('newKeyName'); + }) + .toThrowError( + 'Cannot set expiry key name because no LocalStorageExpiry has been provided.'); + }); + it('setIdle() should throw if argument is less than or equal to zero', () => { let expected = new Error('\'seconds\' must be greater zero'); @@ -146,14 +169,14 @@ describe('core/Idle', () => { }); it('stop() should clear timeouts and stop running', fakeAsync(() => { - spyOn(window, 'clearInterval').and.callThrough(); + spyOn(window, 'clearInterval').and.callThrough(); - instance.watch(); - instance.stop(); + instance.watch(); + instance.stop(); - expect(instance.isRunning()).toBe(false); - expect(window.clearInterval).toHaveBeenCalledTimes(1); - })); + expect(instance.isRunning()).toBe(false); + expect(window.clearInterval).toHaveBeenCalledTimes(1); + })); it('stop() should clear last expiry', () => { instance.watch(); @@ -163,23 +186,23 @@ describe('core/Idle', () => { }); it('watch() should clear timeouts and start running', fakeAsync(() => { - spyOn(window, 'setInterval').and.callThrough(); + spyOn(window, 'setInterval').and.callThrough(); - instance.watch(); + instance.watch(); - expect(instance.isRunning()).toBe(true); - expect(window.setInterval).toHaveBeenCalledTimes(1); + expect(instance.isRunning()).toBe(true); + expect(window.setInterval).toHaveBeenCalledTimes(1); - instance.stop(); - })); + instance.stop(); + })); it('watch() should set expiry', () => { let now = new Date(); expiry.mockNow = now; instance.watch(); expect(expiry.last()) - .toEqual( - new Date(now.getTime() + ((instance.getIdle() + instance.getTimeout()) * 1000))); + .toEqual( + new Date(now.getTime() + ((instance.getIdle() + instance.getTimeout()) * 1000))); }); it('watch() should attach all interrupts', () => { @@ -209,28 +232,28 @@ describe('core/Idle', () => { }); it('watch() should attach all interrupts when resuming after timeout', fakeAsync(() => { - let source = new MockInterruptSource(); + let source = new MockInterruptSource(); - instance.setTimeout(3); - instance.setInterrupts([source]); - instance.watch(); + instance.setTimeout(3); + instance.setInterrupts([source]); + instance.watch(); - expect(source.isAttached).toBe(true); + expect(source.isAttached).toBe(true); - tick(3000); - tick(1000); - tick(1000); - tick(1000); + tick(3000); + tick(1000); + tick(1000); + tick(1000); - expect(instance.isIdling()).toBe(true); - expect(source.isAttached).toBe(false); + expect(instance.isIdling()).toBe(true); + expect(source.isAttached).toBe(false); - instance.watch(); + instance.watch(); - expect(source.isAttached).toBe(true); + expect(source.isAttached).toBe(true); - instance.stop(); - })); + instance.stop(); + })); it('timeout() should detach all interrupts', () => { let source = new MockInterruptSource(); @@ -251,122 +274,122 @@ describe('core/Idle', () => { }); it('isIdle() should return true when idle interval elapses, and false after stop() is called', - fakeAsync(() => { - instance.watch(); - expect(instance.isIdling()).toBe(false); + fakeAsync(() => { + instance.watch(); + expect(instance.isIdling()).toBe(false); - tick(3000); + tick(3000); - expect(instance.isIdling()).toBe(true); + expect(instance.isIdling()).toBe(true); - instance.stop(); - expect(instance.isIdling()).toBe(false); - })); + instance.stop(); + expect(instance.isIdling()).toBe(false); + })); it('should NOT pause interrupts when idle', fakeAsync(() => { - let source = new MockInterruptSource(); + let source = new MockInterruptSource(); - instance.setInterrupts([source]); - instance.watch(); + instance.setInterrupts([source]); + instance.watch(); - tick(3000); + tick(3000); - expect(instance.isIdling()).toBe(true); + expect(instance.isIdling()).toBe(true); - expect(source.isAttached).toBe(true); + expect(source.isAttached).toBe(true); - instance.stop(); - })); + instance.stop(); + })); it('emits an onIdleStart event when the user becomes idle', fakeAsync(() => { - spyOn(instance.onIdleStart, 'emit').and.callThrough(); + spyOn(instance.onIdleStart, 'emit').and.callThrough(); - instance.watch(); - tick(3000); + instance.watch(); + tick(3000); - expect(instance.onIdleStart.emit).toHaveBeenCalledTimes(1); + expect(instance.onIdleStart.emit).toHaveBeenCalledTimes(1); - instance.stop(); - })); + instance.stop(); + })); it('emits an onIdleEnd event when the user returns from idle', fakeAsync(() => { - spyOn(instance.onIdleEnd, 'emit').and.callThrough(); + spyOn(instance.onIdleEnd, 'emit').and.callThrough(); - instance.watch(); - tick(3000); - expect(instance.isIdling()).toBe(true); + instance.watch(); + tick(3000); + expect(instance.isIdling()).toBe(true); - instance.watch(); - expect(instance.onIdleEnd.emit).toHaveBeenCalledTimes(1); + instance.watch(); + expect(instance.onIdleEnd.emit).toHaveBeenCalledTimes(1); - instance.stop(); - })); + instance.stop(); + })); it('emits an onTimeoutWarning every second during the timeout duration', fakeAsync(() => { - spyOn(instance.onTimeoutWarning, 'emit').and.callThrough(); - spyOn(instance.onTimeout, 'emit').and.callThrough(); + spyOn(instance.onTimeoutWarning, 'emit').and.callThrough(); + spyOn(instance.onTimeout, 'emit').and.callThrough(); - instance.setTimeout(3); - instance.watch(); - tick(3000); - expect(instance.isIdling()).toBe(true); + instance.setTimeout(3); + instance.watch(); + tick(3000); + expect(instance.isIdling()).toBe(true); - expect(instance.onTimeoutWarning.emit).toHaveBeenCalledTimes(1); - tick(1000); - expect(instance.onTimeoutWarning.emit).toHaveBeenCalledTimes(2); - tick(1000); - expect(instance.onTimeoutWarning.emit).toHaveBeenCalledTimes(3); - expect(instance.onTimeout.emit).not.toHaveBeenCalled(); + expect(instance.onTimeoutWarning.emit).toHaveBeenCalledTimes(1); + tick(1000); + expect(instance.onTimeoutWarning.emit).toHaveBeenCalledTimes(2); + tick(1000); + expect(instance.onTimeoutWarning.emit).toHaveBeenCalledTimes(3); + expect(instance.onTimeout.emit).not.toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); it('emits an onTimeout event when the countdown reaches 0', fakeAsync(() => { - spyOn(instance.onTimeout, 'emit').and.callThrough(); + spyOn(instance.onTimeout, 'emit').and.callThrough(); - instance.setTimeout(3); - instance.watch(); - tick(3000); - expect(instance.isIdling()).toBe(true); + instance.setTimeout(3); + instance.watch(); + tick(3000); + expect(instance.isIdling()).toBe(true); - tick(1000); // going once - tick(1000); // going twice - tick(1000); // going thrice + tick(1000); // going once + tick(1000); // going twice + tick(1000); // going thrice - expect(instance.onTimeout.emit).toHaveBeenCalledTimes(1); + expect(instance.onTimeout.emit).toHaveBeenCalledTimes(1); - instance.stop(); - })); + instance.stop(); + })); it('does not emit an onTimeoutWarning when timeout is disabled', fakeAsync(() => { - spyOn(instance.onTimeoutWarning, 'emit').and.callThrough(); + spyOn(instance.onTimeoutWarning, 'emit').and.callThrough(); - instance.setTimeout(false); - instance.watch(); - tick(3000); - expect(instance.isIdling()).toBe(true); + instance.setTimeout(false); + instance.watch(); + tick(3000); + expect(instance.isIdling()).toBe(true); - tick(1000); - tick(1000); - expect(instance.onTimeoutWarning.emit).not.toHaveBeenCalled(); + tick(1000); + tick(1000); + expect(instance.onTimeoutWarning.emit).not.toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); it('does not emit an onTimeout event timeout is disabled', fakeAsync(() => { - spyOn(instance.onTimeout, 'emit').and.callThrough(); + spyOn(instance.onTimeout, 'emit').and.callThrough(); - instance.setTimeout(false); - instance.watch(); - tick(3000); - expect(instance.isIdling()).toBe(true); + instance.setTimeout(false); + instance.watch(); + tick(3000); + expect(instance.isIdling()).toBe(true); - tick(3000); + tick(3000); - expect(instance.onTimeout.emit).not.toHaveBeenCalled(); + expect(instance.onTimeout.emit).not.toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); it('interrupt() does not call watch() or emit onInterrupt if not running', () => { spyOn(instance, 'watch').and.callThrough(); @@ -382,117 +405,117 @@ describe('core/Idle', () => { spyOn(instance.onInterrupt, 'emit').and.callThrough(); instance.watch(); - let expected = {test: true}; + let expected = { test: true }; instance.interrupt(false, expected); expect(instance.onInterrupt.emit).toHaveBeenCalledWith(expected); }); it('interrupt() with the force parameter set to true calls watch()', fakeAsync(() => { - instance.setAutoResume(AutoResume.disabled); - instance.setIdle(3); + instance.setAutoResume(AutoResume.disabled); + instance.setIdle(3); - instance.watch(); - spyOn(instance, 'watch').and.callThrough(); - tick(3000); + instance.watch(); + spyOn(instance, 'watch').and.callThrough(); + tick(3000); - expect(instance.isIdling()).toBe(true); + expect(instance.isIdling()).toBe(true); - instance.interrupt(true); + instance.interrupt(true); - expect(instance.watch).toHaveBeenCalled(); + expect(instance.watch).toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); it('interrupt() with AutoResume.disabled should not call watch() when state is idle', - fakeAsync(() => { - instance.setAutoResume(AutoResume.disabled); - instance.setIdle(3); + fakeAsync(() => { + instance.setAutoResume(AutoResume.disabled); + instance.setIdle(3); - instance.watch(); - spyOn(instance, 'watch').and.callThrough(); - tick(3000); + instance.watch(); + spyOn(instance, 'watch').and.callThrough(); + tick(3000); - expect(instance.isIdling()).toBe(true); + expect(instance.isIdling()).toBe(true); - instance.interrupt(); + instance.interrupt(); - expect(instance.watch).not.toHaveBeenCalled(); + expect(instance.watch).not.toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); it('interrupt() with AutoResume.disabled should not call watch() when state is not idle', - fakeAsync(() => { - instance.setAutoResume(AutoResume.disabled); - instance.setIdle(3); + fakeAsync(() => { + instance.setAutoResume(AutoResume.disabled); + instance.setIdle(3); - instance.watch(); - spyOn(instance, 'watch').and.callThrough(); - tick(2000); + instance.watch(); + spyOn(instance, 'watch').and.callThrough(); + tick(2000); - expect(instance.isIdling()).toBe(false); + expect(instance.isIdling()).toBe(false); - instance.interrupt(); + instance.interrupt(); - expect(instance.watch).not.toHaveBeenCalled(); + expect(instance.watch).not.toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); it('interrupt() with AutoResume.idle should call watch when state is idle', fakeAsync(() => { - instance.setAutoResume(AutoResume.idle); - instance.setIdle(3); + instance.setAutoResume(AutoResume.idle); + instance.setIdle(3); - instance.watch(); - spyOn(instance, 'watch').and.callThrough(); - tick(3000); + instance.watch(); + spyOn(instance, 'watch').and.callThrough(); + tick(3000); - expect(instance.isIdling()).toBe(true); + expect(instance.isIdling()).toBe(true); - instance.interrupt(); + instance.interrupt(); - expect(instance.watch).toHaveBeenCalled(); + expect(instance.watch).toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); it('interrupt() with AutoResume.notIdle should call watch() when state is not idle', - fakeAsync(() => { - instance.setAutoResume(AutoResume.notIdle); - instance.setIdle(3); + fakeAsync(() => { + instance.setAutoResume(AutoResume.notIdle); + instance.setIdle(3); - instance.watch(); - spyOn(instance, 'watch').and.callThrough(); - tick(2000); + instance.watch(); + spyOn(instance, 'watch').and.callThrough(); + tick(2000); - expect(instance.isIdling()).toBe(false); + expect(instance.isIdling()).toBe(false); - instance.interrupt(); + instance.interrupt(); - expect(instance.watch).toHaveBeenCalled(); + expect(instance.watch).toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); it('interrupt() with AutoResume.notIdle should not call watch() when state is idle', - fakeAsync(() => { - instance.setAutoResume(AutoResume.notIdle); - instance.setIdle(3); + fakeAsync(() => { + instance.setAutoResume(AutoResume.notIdle); + instance.setIdle(3); - instance.watch(); - spyOn(instance, 'watch').and.callThrough(); - tick(3000); + instance.watch(); + spyOn(instance, 'watch').and.callThrough(); + tick(3000); - expect(instance.isIdling()).toBe(true); + expect(instance.isIdling()).toBe(true); - instance.interrupt(); + instance.interrupt(); - expect(instance.watch).not.toHaveBeenCalled(); + expect(instance.watch).not.toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); it('interrupt() should not call watch if expiry has expired', () => { instance.setTimeout(3); @@ -516,21 +539,21 @@ describe('core/Idle', () => { }); it('triggering an interrupt source should call interrupt()', fakeAsync(() => { - spyOn(instance.onInterrupt, 'emit').and.callThrough(); + spyOn(instance.onInterrupt, 'emit').and.callThrough(); - let source = new MockInterruptSource; - instance.setInterrupts([source]); + let source = new MockInterruptSource; + instance.setInterrupts([source]); - instance.watch(); - source.trigger(); - // not sure why I have to pad the call with a tick for onInterrupt to be called - // possibly because of RxJS throttling - tick(1); + instance.watch(); + source.trigger(); + // not sure why I have to pad the call with a tick for onInterrupt to be called + // possibly because of RxJS throttling + tick(1); - expect(instance.onInterrupt.emit).toHaveBeenCalledTimes(1); + expect(instance.onInterrupt.emit).toHaveBeenCalledTimes(1); - instance.stop(); - })); + instance.stop(); + })); it('ngOnDestroy calls stop() and clearInterrupts()', () => { spyOn(instance, 'stop').and.callThrough(); @@ -548,8 +571,8 @@ describe('core/Idle', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ - MockExpiry, {provide: IdleExpiry, useExisting: MockExpiry}, - {provide: KeepaliveSvc, useClass: MockKeepaliveSvc}, Idle + MockExpiry, { provide: IdleExpiry, useExisting: MockExpiry }, + { provide: KeepaliveSvc, useClass: MockKeepaliveSvc }, Idle ] }); }); @@ -585,68 +608,68 @@ describe('core/Idle', () => { describe('watching', () => { it('should start keepalive when watch() is called', fakeAsync(() => { - instance.watch(); - expect(svc.isRunning).toBe(true); + instance.watch(); + expect(svc.isRunning).toBe(true); - instance.stop(); - })); + instance.stop(); + })); it('should stop keepalive when stop() is called', fakeAsync(() => { - instance.watch(); - expect(svc.isRunning).toBe(true); + instance.watch(); + expect(svc.isRunning).toBe(true); - instance.stop(); + instance.stop(); - expect(svc.isRunning).toBe(false); - })); + expect(svc.isRunning).toBe(false); + })); it('should stop keepalive when idle', fakeAsync(() => { - instance.watch(); - expect(svc.isRunning).toBe(true); - tick(3000); + instance.watch(); + expect(svc.isRunning).toBe(true); + tick(3000); - expect(instance.isIdling()).toBe(true); - expect(instance.isRunning()).toBe(true); - expect(svc.isRunning).toBe(false); + expect(instance.isIdling()).toBe(true); + expect(instance.isRunning()).toBe(true); + expect(svc.isRunning).toBe(false); - instance.stop(); - })); + instance.stop(); + })); it('should stop keepalive when timed out', fakeAsync(() => { - instance.watch(); - expect(svc.isRunning).toBe(true); - tick(3000); - tick(1000); - tick(1000); - tick(1000); + instance.watch(); + expect(svc.isRunning).toBe(true); + tick(3000); + tick(1000); + tick(1000); + tick(1000); - expect(instance.isIdling()).toBe(true); - expect(instance.isRunning()).toBe(false); - expect(svc.isRunning).toBe(false); + expect(instance.isIdling()).toBe(true); + expect(instance.isRunning()).toBe(false); + expect(svc.isRunning).toBe(false); - instance.stop(); - })); + instance.stop(); + })); it('should immediately ping and restart keepalive when user returns from idle', - fakeAsync(() => { - spyOn(svc, 'ping').and.callThrough(); - instance.watch(); - expect(svc.isRunning).toBe(true); - tick(3000); + fakeAsync(() => { + spyOn(svc, 'ping').and.callThrough(); + instance.watch(); + expect(svc.isRunning).toBe(true); + tick(3000); - expect(instance.isIdling()).toBe(true); - expect(instance.isRunning()).toBe(true); - expect(svc.isRunning).toBe(false); + expect(instance.isIdling()).toBe(true); + expect(instance.isRunning()).toBe(true); + expect(svc.isRunning).toBe(false); - instance.interrupt(); + instance.interrupt(); - expect(instance.isIdling()).toBe(false); - expect(instance.isRunning()).toBe(true); - expect(svc.isRunning).toBe(true); - expect(svc.ping).toHaveBeenCalled(); + expect(instance.isIdling()).toBe(false); + expect(instance.isRunning()).toBe(true); + expect(svc.isRunning).toBe(true); + expect(svc.ping).toHaveBeenCalled(); - instance.stop(); - })); + instance.stop(); + })); }); }); }); diff --git a/modules/core/src/idle.ts b/modules/core/src/idle.ts index 713edf4..e754944 100644 --- a/modules/core/src/idle.ts +++ b/modules/core/src/idle.ts @@ -5,6 +5,7 @@ import {Interrupt} from './interrupt'; import {InterruptArgs} from './interruptargs'; import {InterruptSource} from './interruptsource'; import {KeepaliveSvc} from './keepalivesvc'; +import {LocalStorageExpiry} from './localstorageexpiry'; /* @@ -57,6 +58,18 @@ export class Idle implements OnDestroy { } } + /* + * Sets the expiry key name for localStorage. + * @param The name of the expiry key. + */ + setExpiryKey(key: string): void { + if (this.expiry instanceof LocalStorageExpiry) { + this.expiry.setExpiryKey(key); + } else { + throw new Error('Cannot set expiry key name because no LocalStorageExpiry has been provided.'); + } + } + /* * Returns whether or not keepalive integration is enabled. * @return True if integration is enabled; otherwise, false. diff --git a/modules/core/src/idleexpiry.spec.ts b/modules/core/src/idleexpiry.spec.ts index 4ca5681..67e9218 100644 --- a/modules/core/src/idleexpiry.spec.ts +++ b/modules/core/src/idleexpiry.spec.ts @@ -17,6 +17,10 @@ describe('core/IdleExpiry', () => { expect(instance.id()).toBe(expected); }); + it('id() when empty value should throw error', () => { + expect(() => { instance.id(''); }).toThrowError('A value must be specified for the ID.'); + }); + it('id() returns default value', () => { let expected = new Date(); jasmine.clock().mockDate(expected); diff --git a/modules/core/src/localstorageexpiry.spec.ts b/modules/core/src/localstorageexpiry.spec.ts index 7b86612..850f316 100644 --- a/modules/core/src/localstorageexpiry.spec.ts +++ b/modules/core/src/localstorageexpiry.spec.ts @@ -21,6 +21,24 @@ describe('core/LocalStorageExpiry', () => { expect(service.last()).toEqual(expected); })); + it('setExpiryKey() sets the key name of expiry', inject([LocalStorageExpiry], (service: LocalStorageExpiry) => { + expect(service.getExpiryKey()).toBe('expiry'); + service.setExpiryKey('name'); + expect(service.getExpiryKey()).toBe('name'); + })); + + it('setExpiryKey() doesn\'t set expiry key name if param is null', inject([LocalStorageExpiry], (service: LocalStorageExpiry) => { + expect(service.getExpiryKey()).toBe('expiry'); + service.setExpiryKey(null); + expect(service.getExpiryKey()).toBe('expiry'); + })); + + it('setExpiryKey() doesn\'t set expiry key name if param is empty', inject([LocalStorageExpiry], (service: LocalStorageExpiry) => { + expect(service.getExpiryKey()).toBe('expiry'); + service.setExpiryKey(''); + expect(service.getExpiryKey()).toBe('expiry'); + })); + }); function testBedConfiguration() { diff --git a/modules/core/src/localstorageexpiry.ts b/modules/core/src/localstorageexpiry.ts index e5ab873..263b1a8 100644 --- a/modules/core/src/localstorageexpiry.ts +++ b/modules/core/src/localstorageexpiry.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, Optional } from '@angular/core'; import { IdleExpiry } from './idleexpiry'; import { LocalStorage } from './localstorage'; @@ -9,6 +9,8 @@ import { LocalStorage } from './localstorage'; @Injectable() export class LocalStorageExpiry extends IdleExpiry { + private expiryKey: string = 'expiry'; + constructor(private localStorage: LocalStorage) { super(); } @@ -26,8 +28,26 @@ export class LocalStorageExpiry extends IdleExpiry { return this.getExpiry(); } + /* + * Gets the expiry key name. + * @return The name of the expiry key. + */ + getExpiryKey(): string { + return this.expiryKey; + } + + /* + * Sets the expiry key name. + * @param The name of the expiry key. + */ + setExpiryKey(key: string): void { + if (key) { + this.expiryKey = key; + } + } + private getExpiry(): Date { - let expiry: string = this.localStorage.getItem('expiry'); + let expiry: string = this.localStorage.getItem(this.expiryKey); if (expiry) { return new Date(parseInt(expiry, 10)); } else { @@ -36,7 +56,7 @@ export class LocalStorageExpiry extends IdleExpiry { } private setExpiry(value: Date) { - this.localStorage.setItem('expiry', value.getTime().toString()); + this.localStorage.setItem(this.expiryKey, value.getTime().toString()); } } diff --git a/modules/core/src/storageinterruptsource.spec.ts b/modules/core/src/storageinterruptsource.spec.ts index 253dde3..d488ccd 100644 --- a/modules/core/src/storageinterruptsource.spec.ts +++ b/modules/core/src/storageinterruptsource.spec.ts @@ -11,8 +11,8 @@ describe('core/StorageInterruptSource', () => { let init: StorageEventInit = { key: 'ng2Idle.expiry', - oldValue: null, newValue: '', + oldValue: null, url: 'http://localhost:4200/' }; @@ -44,9 +44,9 @@ describe('core/StorageInterruptSource', () => { source.attach(); let init: StorageEventInit = { - key: 'ng2Idle.otherKey', - oldValue: null, + key: 'otherKey', newValue: '', + oldValue: null, url: 'http://localhost:4200/' }; diff --git a/modules/core/src/storageinterruptsource.ts b/modules/core/src/storageinterruptsource.ts index ff55f7e..35bce6f 100644 --- a/modules/core/src/storageinterruptsource.ts +++ b/modules/core/src/storageinterruptsource.ts @@ -14,7 +14,7 @@ export class StorageInterruptSource extends WindowInterruptSource { * @return True if the event should be filtered (don't cause an interrupt); otherwise, false. */ filterEvent(event: StorageEvent): boolean { - if (event.key === 'ng2Idle.expiry') { + if (event.key.indexOf('ng2Idle.') >= 0) { return false; } return true;