Skip to content

Commit 01279ad

Browse files
author
Christopher Goddard
authored
[VEG-874][VEG-754][VEG-604][VEG-631] modernise babel tooling, support TypeScript, and add idleState to the zaf_sdk to facilitate session tracking (#145)
* WIP: adding idleState to the zaf_sdk * Run 'npm install' * Run 'npx standard --fix' + fix minor oversight * Start to add Typescript support * Continue working at adding typescript support * Continue attempting to add Typescript support * Continue with adding typescript support ... * Downgrade ts-loader to 8.3.0 * Remove unnecessary code * Bump webpack so can run 'npm run server' * Update uglify-js-plugin for webpack so can 'npm run server' without issue * Add additional github actions on branch push * Bump karma, add uglify plugin * Move more webpack options to common config to be run in test mode as well * Alter build script per change to webpack api * Remove infinite loop task from github action workflow * Remove nontest config object entirely to fix minified sourcemap issue * Pass session_state_timeout as optional option * Reword get idle state method and alter comments so as to clarify intent * Remove default_idle_timeout constant from idleState util * Remove unnecessary _removeObserverCallback attribute on IdleState class * This block not needed as observer emits required information * Handle iframe_session_timeout attribute passed on runningApp registration from ZAF * Set the idleState object on app.registered only * Simplify implementation as IdleState class is only called once in the zaf_sdk * Add sourceMap output filename attribute to outputs * Migrate from Uglify (deprecated) to Terser * npm install * npx standard --fix * npm install terser-webpack-plugin --save-dev * npm install --save babel-runtime * npm install @types/node --save-dev * npm i core-js --save * Modernise babel compilation tooling * Remove explicit reference to core-js * Dont forget the .js before the .map * Add options to @babel/preset-env * Simplify determination as to whether we are in a test environment
1 parent 9c786d6 commit 01279ad

File tree

10 files changed

+4366
-2541
lines changed

10 files changed

+4366
-2541
lines changed

.babelrc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"plugins": ["@babel/plugin-transform-runtime"],
3+
"presets": [
4+
["@babel/preset-env", {
5+
"targets": {
6+
"esmodules": true,
7+
"browsers": [
8+
"last 2 versions",
9+
"last 1 ie version"
10+
]
11+
}
12+
}]
13+
]
14+
}

.github/workflows/actions.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ jobs:
1818
- env:
1919
TASK: test
2020
command: npm test
21+
22+
- env:
23+
TASK: build
24+
command: npm run build
2125

2226
steps:
2327
- uses: zendesk/checkout@v2

lib/client.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
/* global URL */
22
import version from 'version'
33
import { when, isObject, isString } from './utils'
4+
import IdleState from './utils/idleState'
45
import Tracker from './tracker'
56
import NativePromise from 'native-promise-only'
67

78
const Promise = window.Promise || NativePromise
9+
10+
export const IFRAME_SESSION_TIMEOUT = 5 * 60 * 1000 // Set timeout for iframe sessions to 5 minutes.
811
export const PROMISE_TIMEOUT = 10000
912
// 10 seconds, see ZD#4058685
1013
export const PROMISE_TIMEOUT_LONG = 5 * 60 * 1000
@@ -328,6 +331,8 @@ export default class Client {
328331
this._instanceClients = {}
329332
this._metadata = null
330333
this._context = options.context || null
334+
this._iframe_session_timeout = options.iframe_session_timeout || IFRAME_SESSION_TIMEOUT
335+
this._idleState = null
331336
this.ready = false
332337

333338
if (!isOriginValid(this._origin)) {
@@ -347,6 +352,20 @@ export default class Client {
347352
this.ready = true
348353
this._metadata = data.metadata
349354
this._context = data.context
355+
this._iframe_session_timeout = data.iframe_session_timeout
356+
this._idleState = new IdleState(data.iframe_session_timeout)
357+
// Add an observer. This will be called when the idleState changes
358+
this._idleState.addObserver(state => {
359+
if (state === 'active') {
360+
// ... application is now active, timeout clock is reset to zero
361+
// Send a postMessage indicating liveness + possibly a payload too
362+
this.postMessage('session.live')
363+
} else if (state === 'idle') {
364+
// ... application is now idle, timeout clock is ticking
365+
// Send a postMessage indicating sessionTimeout + possibly a payload too
366+
this.postMessage('session.idle')
367+
}
368+
})
350369
}, this)
351370

352371
this.on('context.updated', (context) => {

lib/utils/environment.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const NODE_ENV = process.env['NODE_ENV'];
2+
3+
export const isProduction = NODE_ENV === 'production';
4+
5+
export const isDevelopment = NODE_ENV === 'development';
6+
7+
export const isTest = NODE_ENV === 'test';

lib/utils/idleState.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import Observers, { ListenerFunction } from './listeners';
2+
3+
/*
4+
IdleState let you know if the application is active or idle. The application is considered idle if not user
5+
interactions have been detected for a specific amount of time (timeout). To use IdleState, you first create an
6+
IdleState instance, then you can either get the current state from the instance or add an observer to be called each
7+
time the state changes.
8+
9+
When creating an instance, you can provide a timeout, by default timeout of 3 minutes is used. When done with the
10+
IdleState instance, you must call the delete method to release it.
11+
12+
If several consumers create an idleState with the same timeout, only one instance is created. The instance is shared
13+
between the consumers.
14+
15+
Syntax:
16+
new IdleState();
17+
new IdleState(timeout);
18+
19+
timeout: timeout in milliseconds, default is 3 minutes.
20+
21+
Properties:
22+
IdleState.state:
23+
current state as string, either 'active' or 'idle', read only.
24+
25+
Methods:
26+
IdleState.addObserver(observer):
27+
Add an observer to be called when the idle state changes. The observer receives the new state as parameter. You
28+
can several observers.
29+
Returns a function to be called to remove the observer.
30+
31+
IdleState.delete():
32+
Release the IdleState instance. As the same instance can be shared with multiple consumers, it's important to call
33+
delete() when done with it. But first, you should remove any observer you have added, else they will still be
34+
called until all consumers have released the instance.
35+
36+
Usage:
37+
import IdleState from 'utils/IdleState';
38+
39+
// Create an instance with a timeout of 2 minutes
40+
const idleState = new IdleState(2 * 60 * 1000);
41+
42+
// Add an observer
43+
const removeObserver = idleState.addObserver(state => {
44+
if (state === 'active') {
45+
// ... application is now active
46+
} else if (state === 'idle') {
47+
// ... application is now idle
48+
}
49+
});
50+
51+
// Read the current state
52+
const currentState = idleState.state;
53+
54+
// When done, release the instance, but first remove the observer
55+
removeObserver();
56+
idleState.delete();
57+
*/
58+
59+
import { isTest } from './environment';
60+
export const STATE_ACTIVE = 'active';
61+
export const STATE_IDLE = 'idle';
62+
63+
const USER_EVENTS = ['mousemove', 'keydown', 'wheel', 'mousedown', 'touchstart', 'touchmove'];
64+
// list of user events to ignore when window hasn't focus as string, i.e. 'mousemove touchmove'
65+
const IGNORED_USER_EVENTS = 'mousemove';
66+
67+
const stateProperty = Symbol('state');
68+
69+
const setState = (idleState: IdleState, state: 'active' | 'idle') => {
70+
if (idleState[stateProperty] !== state) {
71+
idleState[stateProperty] = state;
72+
idleState.observers && idleState.observers.call(state);
73+
}
74+
};
75+
class IdleState {
76+
refcount?: number;
77+
timeout?: number;
78+
[stateProperty]: 'active' | 'idle';
79+
hasActiveEvent?: boolean;
80+
timer?: number | null;
81+
hasFocus?: boolean;
82+
userEventListenerAdded?: boolean;
83+
observers?: Observers;
84+
setState?: (state: 'active' | 'idle') => void;
85+
86+
constructor(timeout) {
87+
this.timeout = timeout;
88+
this[stateProperty] = STATE_ACTIVE;
89+
this.hasActiveEvent = true;
90+
this.timer = null;
91+
this.hasFocus = true;
92+
this.userEventListenerAdded = false;
93+
this.observers = new Observers();
94+
95+
this.refcount = 1;
96+
97+
// Bind callbacks
98+
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
99+
this.handleFocusChange = this.handleFocusChange.bind(this);
100+
this.handleUserEvent = this.handleUserEvent.bind(this);
101+
this.handleTimer = this.handleTimer.bind(this);
102+
103+
// Install listeners and timer
104+
document.addEventListener('visibilitychange', this.handleVisibilityChange, true);
105+
106+
// Because Safari dispatches focus events before visibilitychange, we need to register focus/blurr listener here
107+
// and not inside handleVisibilityChange with the other event listeners
108+
window.addEventListener('focus', this.handleFocusChange, true);
109+
window.addEventListener('blur', this.handleFocusChange, true);
110+
111+
this.handleVisibilityChange();
112+
this.resetTimer();
113+
114+
// Add access to setState for testing
115+
if (isTest) {
116+
this.setState = (state) => {
117+
setState(this, state);
118+
};
119+
}
120+
}
121+
122+
delete(): void {
123+
if (this.refcount !== undefined && --this.refcount === 0) {
124+
document.removeEventListener('visibilitychange', this.handleVisibilityChange, true);
125+
window.removeEventListener('focus', this.handleFocusChange, true);
126+
window.removeEventListener('blur', this.handleFocusChange, true);
127+
128+
if (this.userEventListenerAdded) {
129+
USER_EVENTS.forEach((type) =>
130+
document.removeEventListener(type, this.handleUserEvent, true)
131+
);
132+
}
133+
134+
this.clearTimer();
135+
delete this.observers;
136+
}
137+
}
138+
139+
get state(): 'active' | 'idle' {
140+
return this[stateProperty];
141+
}
142+
143+
clearTimer(): void {
144+
if (this.timer) {
145+
clearTimeout(this.timer);
146+
this.timer = undefined;
147+
}
148+
}
149+
150+
resetTimer(): void {
151+
this.clearTimer();
152+
this.timer = window.setTimeout(this.handleTimer, this.timeout);
153+
}
154+
155+
handleTimer(): void {
156+
this.resetTimer();
157+
158+
if (this.hasActiveEvent) {
159+
// we had user events during last timeout cycle,
160+
// do not set state to idle yet
161+
this.hasActiveEvent = false;
162+
} else {
163+
// we did not have user event during last timeout cycle,
164+
// set state to idle now
165+
setState(this, STATE_IDLE);
166+
}
167+
}
168+
169+
markActive(): void {
170+
this.hasActiveEvent = true;
171+
setState(this, STATE_ACTIVE);
172+
}
173+
174+
handleUserEvent(event: Event): void {
175+
if (!this.hasFocus && IGNORED_USER_EVENTS.includes(event.type)) return;
176+
177+
this.markActive();
178+
}
179+
180+
handleFocusChange(event: Event): void {
181+
// we only care about focus/blur event related to the window itself
182+
if (event.target !== window) return;
183+
184+
this.hasFocus = event.type === 'focus';
185+
if (this.hasFocus) {
186+
this.markActive();
187+
}
188+
}
189+
190+
handleVisibilityChange(): void {
191+
if (document.hidden) {
192+
if (this.userEventListenerAdded) {
193+
USER_EVENTS.forEach((type) =>
194+
document.removeEventListener(type, this.handleUserEvent, true)
195+
);
196+
this.userEventListenerAdded = false;
197+
}
198+
} else if (!this.userEventListenerAdded) {
199+
USER_EVENTS.forEach((type) => document.addEventListener(type, this.handleUserEvent, true));
200+
this.userEventListenerAdded = true;
201+
}
202+
}
203+
204+
addObserver(observer: ListenerFunction): (() => void) | undefined {
205+
return this.observers && this.observers.add(observer);
206+
}
207+
}
208+
209+
export default IdleState;

lib/utils/listeners.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export type ListenerFunction = (...args: unknown[]) => void;
2+
3+
interface Listener {
4+
fn: ListenerFunction;
5+
count: number;
6+
}
7+
8+
class Listeners {
9+
private listeners: Listener[];
10+
11+
constructor() {
12+
this.listeners = [];
13+
}
14+
15+
add(listener: ListenerFunction): () => void {
16+
const find = (listener: ListenerFunction) =>
17+
this.listeners.findIndex((item) => item.fn === listener);
18+
19+
const index = find(listener);
20+
if (index === -1) {
21+
this.listeners.push({ fn: listener, count: 1 });
22+
} else {
23+
const listener = this.listeners[index];
24+
listener && listener.count++;
25+
}
26+
27+
return () => {
28+
const index = find(listener);
29+
30+
if (index !== -1) {
31+
const listener = this.listeners[index];
32+
listener && listener.count--;
33+
if (listener && listener.count === 0) {
34+
this.listeners.splice(index, 1);
35+
}
36+
}
37+
};
38+
}
39+
40+
call(...args: unknown[]): void {
41+
this.listeners.forEach((listener) => listener.fn(...args));
42+
}
43+
}
44+
45+
export default Listeners;

0 commit comments

Comments
 (0)