Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC Use WebWorker for reliable timers #1450

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion jest.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ const config = Object.assign({}, baseConfig, {
globals: Object.assign({}, baseConfig.globals, {
USER_AGENT
}),
setupFiles: baseConfig.setupFiles.concat([
'jsdom-worker'
]),
testPathIgnorePatterns: baseConfig.testPathIgnorePatterns.concat([
'<rootDir>/test/spec/serverStorage.js',
'<rootDir>/test/spec/features/server'
]),
moduleNameMapper: Object.assign({}, baseConfig.moduleNameMapper, {
'^./node$': './browser'
'^./node$': './browser',
'workers/(.*?).emptyWorker': '<rootDir>/build/esm/browser/workers/$1.worker.js',
})
});

Expand Down
13 changes: 8 additions & 5 deletions lib/oidc/TokenManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import { REFRESH_TOKEN_STORAGE_KEY, TOKEN_STORAGE_NAME } from '../constants';
import { EventEmitter } from '../base/types';
import { StorageOptions, StorageProvider, StorageType } from '../storage/types';
import { TimerService } from '../services/TimerService';

const DEFAULT_OPTIONS = {
// TODO: remove in next major version - OKTA-473815
Expand Down Expand Up @@ -76,6 +77,7 @@ export class TokenManager implements TokenManagerInterface {
private storage: StorageProvider;
private state: TokenManagerState;
private options: TokenManagerOptions;
private timerService: TimerService;

on(event: typeof EVENT_RENEWED, handler: TokenManagerRenewEventHandler, context?: object): void;
on(event: typeof EVENT_ERROR, handler: TokenManagerErrorEventHandler, context?: object): void;
Expand Down Expand Up @@ -132,6 +134,7 @@ export class TokenManager implements TokenManagerInterface {
this.storage = sdk.storageManager.getTokenStorage({...storageOptions, useSeparateCookies: true});
this.clock = SdkClock.create(/* sdk, options */);
this.state = defaultState();
this.timerService = new TimerService();
}

start() {
Expand All @@ -141,7 +144,7 @@ export class TokenManager implements TokenManagerInterface {
this.setExpireEventTimeoutAll();
this.state.started = true;
}

stop() {
this.clearExpireEventTimeoutAll();
this.state.started = false;
Expand Down Expand Up @@ -187,7 +190,7 @@ export class TokenManager implements TokenManagerInterface {
}

clearExpireEventTimeout(key) {
clearTimeout(this.state.expireTimeouts[key] as any);
this.timerService.clearTimeout(this.state.expireTimeouts[key] as any);
delete this.state.expireTimeouts[key];

// Remove the renew promise (if it exists)
Expand Down Expand Up @@ -215,14 +218,14 @@ export class TokenManager implements TokenManagerInterface {
// Clear any existing timeout
this.clearExpireEventTimeout(key);

var expireEventTimeout = setTimeout(() => {
var expireEventTimeout = this.timerService.setTimeout(() => {
this.emitExpired(key, token);
}, expireEventWait);

// Add a new timeout
this.state.expireTimeouts[key] = expireEventTimeout;
}

setExpireEventTimeoutAll() {
var tokenStorage = this.storage.getStorage();
for(var key in tokenStorage) {
Expand Down
99 changes: 99 additions & 0 deletions lib/services/TimerService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import timerWorker from '../workers/TimerWorker.emptyWorker';

/* global BUNDLER */

export interface TimerWorkerOutMessage {
action: 'timeoutCallback' | 'init',
timerId?: number,
}
export interface TimerWorkerInMessage {
action: 'setTimeout' | 'clearTimeout',
timerId: number,
timeout?: number,
}

export class TimerService {
private timerWorker?: Worker;
private timersHandlers: Record<number, () => void>;
private timerId: number;

constructor() {
this.timersHandlers = {};
this.timerId = 0;

if (typeof Worker !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (BUNDLER === 'webpack') {
// webpack build (umd/cdn)
const TimerWorker = timerWorker as any;
this.timerWorker = new TimerWorker() as Worker;
} else {
if (!timerWorker?.workerSrc) {
// babel build (cjs), for node - use fallback
} else {
// rollup build (esm)
this.timerWorker = new Worker(this.getWorkerURL());
}
}

this.timerWorker?.addEventListener('message', this.handleWorkerEvent.bind(this));
}
}

private getWorkerURL(): string {
const workerBlob = new Blob([timerWorker.workerSrc], { type: 'text/javascript' });
// eslint-disable-next-line compat/compat
return URL.createObjectURL(workerBlob);
}

private handleWorkerEvent(ev: MessageEvent<TimerWorkerOutMessage>) {
const data = ev.data;
switch(data.action) {
case 'timeoutCallback':
this.handleTimeoutCallback(data);
break;
case 'init':
break;
default:
break;
}
}

private handleTimeoutCallback(data: TimerWorkerOutMessage) {
const { timerId } = data;
const handler = this.timersHandlers[timerId!];
if (handler) {
handler();
}
}

setTimeout(handler: () => void, timeout: number) {
if (this.timerWorker) {
const timerId = this.timerId++;
this.timersHandlers[timerId] = handler.bind(this);
this.timerWorker?.postMessage({
action: 'setTimeout',
timeout,
timerId: timerId,
} as TimerWorkerInMessage);
return timerId;
} else {
// fallback
return setTimeout(handler, timeout);
}
}

clearTimeout(timerId: number) {
if (this.timerWorker) {
this.timerWorker?.postMessage({
action: 'clearTimeout',
timerId: timerId,
} as TimerWorkerInMessage);
delete this.timersHandlers[this.timerId];
} else {
// fallback
return clearTimeout(timerId);
}
}
}
2 changes: 2 additions & 0 deletions lib/types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

declare const SDK_VERSION: string;

declare const BUNDLER: string;

declare interface PromiseConstructor {
// eslint-disable-next-line max-len, @typescript-eslint/member-delimiter-style
allSettled(promises: Array<Promise<any>>): Promise<Array<{status: 'fulfilled' | 'rejected', value?: any, reason?: any}>>;
Expand Down
2 changes: 2 additions & 0 deletions lib/workers/TimerWorker.emptyWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const workerSrc = '';
export default { workerSrc };
45 changes: 45 additions & 0 deletions lib/workers/TimerWorker.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// /// <reference no-default-lib="true"/>
// /// <reference lib="webworker" />
// declare var self: DedicatedWorkerGlobalScope;
import type { TimerWorkerInMessage, TimerWorkerOutMessage } from '../services/TimerService';

const timerIdToTimeout = {};

function handleSetTimeout(data: TimerWorkerInMessage) {
const { timerId, timeout } = data;
const timeoutId = setTimeout(() => {
self.postMessage({
action: 'timeoutCallback',
timerId,
} as TimerWorkerOutMessage);
delete timerIdToTimeout[timerId];
}, timeout);
timerIdToTimeout[timerId] = timeoutId;
}

function handleClearTimeout(data: TimerWorkerInMessage) {
const { timerId } = data;
const timeoutId = timerIdToTimeout[timerId];
if (timeoutId) {
clearTimeout(timeoutId);
delete timerIdToTimeout[timerId];
}
}

self.addEventListener('message', function(ev: MessageEvent<TimerWorkerInMessage>) {
const data = ev.data;
switch (data.action) {
case 'setTimeout':
handleSetTimeout.call(this, data);
break;
case 'clearTimeout':
handleClearTimeout.call(this, data);
break;
default:
break;
}
});

self.postMessage({
action: 'init',
} as TimerWorkerOutMessage);
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
"@rollup/plugin-alias": "^3.1.8",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-replace": "^3.0.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@tsd/typescript": "^4.6.4",
"@types/jest": "^27.5.1",
"@types/node": "^14.0.3",
Expand Down Expand Up @@ -208,6 +209,7 @@
"jest-runner": "^28.1.0",
"jest-runner-tsd": "^3.0.0",
"json-loader": "0.5.4",
"jsdom-worker": "denysoblohin-okta/jsdom-worker#sync-blob",
"lodash": "4.17.21",
"rollup": "^2.70.2",
"rollup-plugin-cleanup": "^3.2.1",
Expand All @@ -216,6 +218,7 @@
"rollup-plugin-typescript2": "^0.30.0",
"rollup-plugin-visualizer": "~5.5.4",
"shelljs": "0.8.5",
"string-replace-loader": "^3.1.0",
"ts-jest": "^28.0.2",
"tsd": "^0.17.0",
"typedoc": "^0.23.19",
Expand All @@ -224,7 +227,8 @@
"webpack": "^5.78.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.2"
"webpack-dev-server": "^4.9.2",
"worker-loader": "^3.0.8"
},
"jest-junit": {
"outputDirectory": "./build2/reports/unit/",
Expand Down
41 changes: 35 additions & 6 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@ import replace from '@rollup/plugin-replace';
import alias from '@rollup/plugin-alias';
import cleanup from 'rollup-plugin-cleanup';
import typescript from 'rollup-plugin-typescript2';
import { getBabelOutputPlugin } from '@rollup/plugin-babel';
import license from 'rollup-plugin-license';
import multiInput from 'rollup-plugin-multi-input';
import { visualizer } from 'rollup-plugin-visualizer';
import resolve from '@rollup/plugin-node-resolve';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import pkg from './package.json';

const path = require('path');

let platforms = ['browser', 'node'];
let entries = {
'worker': 'lib/workers/TimerWorker.worker.ts',
'okta-auth-js': 'lib/exports/default.ts',
'core': 'lib/exports/core.ts',
'authn': 'lib/exports/authn.ts',
'idx': 'lib/exports/idx.ts',
'myaccount': 'lib/exports/myaccount.ts'
'myaccount': 'lib/exports/myaccount.ts',
};
let preserveModules = true;
const combinedOutputDir = true; // all entries share an output dir
Expand Down Expand Up @@ -89,6 +92,7 @@ const getPlugins = (env, entryName) => {
let plugins = [
replace({
'SDK_VERSION': JSON.stringify(pkg.version),
'BUNDLER': JSON.stringify('rollup'),
'global.': 'window.',
preventAssignment: true
}),
Expand All @@ -97,18 +101,25 @@ const getPlugins = (env, entryName) => {
{ find: /.\/node$/, replacement: './browser' }
]
})),
entryName === 'worker' && {
name: 'worker-to-string',
renderChunk(code) {
return `var workerSrc = \`${code}\`; export default { workerSrc };`;
},
},
typescript({
// eslint-disable-next-line node/no-unpublished-require
typescript: require('typescript'),
tsconfigOverride: {
compilerOptions: {
sourceMap: true,
target: 'ES2017', // skip async/await transpile,
target: entryName === 'worker' ? 'ES3' : 'ES2017', // skip async/await transpile,
module: 'ES2020', // support dynamic import
declaration: false
}
}
}),
entryName === 'worker' && resolve(),
cleanup({
extensions,
comments: 'none'
Expand All @@ -120,10 +131,14 @@ const getPlugins = (env, entryName) => {
}
}
}),
replace({
'TimerWorker.emptyWorker.js': 'TimerWorker.worker.js',
preventAssignment: true
}),
multiInput({
relative: 'lib/',
}),
createPackageJson(outputDir)
createPackageJson(outputDir),
];

// if ANALZYE env var is passed, output analyzer html
Expand All @@ -140,19 +155,33 @@ const getPlugins = (env, entryName) => {
return plugins;
};


export default Object.keys(entries).reduce((res, entryName) => {
const entryValue = entries[entryName];
return res.concat(platforms.map((type) => {
return {
return ({
input: Array.isArray(entryValue) ? entryValue : [entryValue],
external: makeExternalPredicate(type),
plugins: getPlugins(type, entryName),
output: [
{
...output,
dir: getOuptutDir(entryName, type)
dir: getOuptutDir(entryName, type),
...(entryName === 'worker' ? {
plugins: [
getBabelOutputPlugin({
presets: [
'@babel/preset-env',
],
})
],
preserveModules: false,
sourcemap: false, // sourcemaps do not work with `worker-to-string` plugin
exports: 'none',
} : {
})
}
]
};
});
}));
}, []);
2 changes: 1 addition & 1 deletion scripts/lint.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash -e
#!/bin/bash -x

source ${OKTA_HOME}/${REPO}/scripts/setup.sh

Expand Down
2 changes: 1 addition & 1 deletion scripts/publish.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash -xe
#!/bin/bash -x

source ${OKTA_HOME}/${REPO}/scripts/setup.sh

Expand Down
Loading