Skip to content

Commit

Permalink
STCOR-759 read okapi config from micro-stripes-config (#1366)
Browse files Browse the repository at this point in the history
A service worker's global state is reset after each sleep/wake cycle,
meaning the `okapiUrl` and `okapiTenant` values so lovingly sent to the
service worker during registration are likely to be promptly forgotten
as soon as the browser is idle for a few minutes and decides it would be
good to clean up inactive processes.

Here, those values are directly imported from a virtual module created
at build-time by stripes-webpack, which forwards the values from the
stripes-config object (most likely, the `stripes.config.js` file) and
allows them to be compiled directly into the generated
`service-worker.js` asset.

An alternative approach would be to pass in those values as URL
parameters when the service worker is registered.

h/t @mkuklis and @JohnC-80 who did the heavy lifting here, both in
thinking through the potential solutions and actually figuring out how
to implement this in our highly customized build process.

* Requires folio-org/stripes-webpack/pull/132

Refs STCOR-759

---------

Co-authored-by: Michal Kuklis <[email protected]>
  • Loading branch information
zburke and mkuklis authored Nov 8, 2023
1 parent 607dc5c commit 6c6a85e
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 32 deletions.
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,7 @@ export { userLocaleConfig } from './src/loginServices';
export * from './src/consortiaServices';
export { default as queryLimit } from './src/queryLimit';
export { default as init } from './src/init';

/* RTR and service worker */
export { postTokenExpiration } from './src/loginServices';
export { registerServiceWorker, unregisterServiceWorker } from './src/serviceWorkerRegistration';
12 changes: 7 additions & 5 deletions src/loginServices.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ function dispatchLocale(url, store, tenant) {
.then((response) => {
if (response.status === 200) {
response.json().then((json) => {
if (json.configs.length) {
if (json.configs?.length) {
const localeValues = JSON.parse(json.configs[0].value);
const { locale, timezone, currency } = localeValues;
if (locale) {
Expand Down Expand Up @@ -258,7 +258,7 @@ export function getPlugins(okapiUrl, store, tenant) {
.then((response) => {
if (response.status < 400) {
response.json().then((json) => {
const configs = json.configs.reduce((acc, val) => ({
const configs = json.configs?.reduce((acc, val) => ({
...acc,
[val.configName]: val.value,
}), {});
Expand Down Expand Up @@ -291,7 +291,7 @@ export function getBindings(okapiUrl, store, tenant) {
} else {
response.json().then((json) => {
const configs = json.configs;
if (configs.length > 0) {
if (Array.isArray(configs) && configs.length > 0) {
const string = configs[0].value;
try {
const tmp = JSON.parse(string);
Expand Down Expand Up @@ -387,10 +387,12 @@ export async function logout(okapiUrl, store) {
/**
* postTokenExpiration
* send SW a TOKEN_EXPIRATION message
* @param {object} tokenExpiration shaped like { atExpires, rtExpires} where both are millisecond timestamps
*
* @returns {Promise}
*/
const postTokenExpiration = (tokenExpiration) => {
if ('serviceWorker' in navigator) {
export const postTokenExpiration = (tokenExpiration) => {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
return navigator.serviceWorker.ready
.then((reg) => {
const sw = reg.active;
Expand Down
2 changes: 2 additions & 0 deletions src/loginServices.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ describe('createOkapiSession', () => {

const postMessage = jest.fn();
navigator.serviceWorker = {
controller: true,
ready: Promise.resolve({
active: {
postMessage,
Expand Down Expand Up @@ -309,6 +310,7 @@ describe('validateUser', () => {

const postMessage = jest.fn();
navigator.serviceWorker = {
controller: true,
ready: Promise.resolve({
active: {
postMessage,
Expand Down
19 changes: 8 additions & 11 deletions src/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
/**
* TLDR: perform refresh-token-rotation for Okapi-bound requests.
*
* Critical reading:
* @see https://web.dev/articles/service-worker-mindset#watch_out_for_global_state
* @see https://web.dev/articles/service-worker-lifecycle#shift-reload
*
* The (rather opaque) specification:
* @see https://www.w3.org/TR/service-workers/
*
* The gory details:
* This service worker acts as a proxy betwen the browser and the network,
* intercepting all fetch requests. Those not bound for Okapi are simply
Expand Down Expand Up @@ -43,15 +50,11 @@
*
*/

import { okapiUrl, okapiTenant } from 'micro-stripes-config';

/** { atExpires, rtExpires } both are JS millisecond timestamps */
let tokenExpiration = null;

/** string FQDN including protocol, e.g. https://some-okapi.somewhere.org */
let okapiUrl = null;

let okapiTenant = null;

/** whether to emit console logs */
let shouldLog = false;

Expand Down Expand Up @@ -455,12 +458,6 @@ self.addEventListener('message', (event) => {
if (event.data.source === '@folio/stripes-core') {
if (shouldLog) console.info('-- (rtr-sw) reading', event.data);

// OKAPI_CONFIG
if (event.data.type === 'OKAPI_CONFIG') {
okapiUrl = event.data.value.url;
okapiTenant = event.data.value.tenant;
}

// LOGGER_CONFIG
if (event.data.type === 'LOGGER_CONFIG') {
shouldLog = !!event.data.value.categories?.split(',').some(cat => cat === 'rtr-sw');
Expand Down
47 changes: 42 additions & 5 deletions src/service-worker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ describe('isOkapiRequest', () => {
});

describe('passThroughLogout', () => {
it('succeeds', async () => {
it('resolves on success', async () => {
const val = { monkey: 'bagel' };
global.fetch = jest.fn(() => (
Promise.resolve({
Expand All @@ -215,14 +215,17 @@ describe('passThroughLogout', () => {
expect(await res.json()).toMatchObject(val);
});

it('succeeds even when it fails', async () => {
it('rejects on failure', async () => {
window.Response = jest.fn();
const val = {};
global.fetch = jest.fn(() => Promise.reject(Promise.resolve(new Response({}))));
global.fetch = jest.fn(() => Promise.reject(Promise.resolve(new Response(JSON.stringify({})))));

const event = { request: 'monkey' };
const res = await passThroughLogout(event);
expect(await res).toMatchObject(val);
try {
await passThroughLogout(event);
} catch (e) {
expect(e).toMatchObject(val);
}
});
});

Expand Down Expand Up @@ -506,6 +509,40 @@ describe('rtr', () => {
expect(e.message).toMatch(error);
}
});

it.skip('foo', async () => {
const foo = handleTokenExpiration;
const bar = messageToClient;

handleTokenExpiration = jest.fn();
messageToClient = jest.fn();

const oUrl = 'https://trinity.edu';
const req = { url: `${oUrl}/manhattan` };
const event = {
request: {
clone: () => req,
}
};

window.Response = jest.fn();

global.fetch = jest.fn()
.mockReturnValueOnce(Promise.resolve({
ok: true,
json: () => Promise.resolve({
accessTokenExpiration: Date.now(),
refreshTokenExpiration: Date.now(),
})
}));

await rtr(event);
expect(handleTokenExpiration).toHaveBeenCalled();
expect(messageToClient).toHaveBeenCalled();

handleTokenExpiration = foo;
messageToClient = bar;
});
});

describe('handleTokenExpiration', () => {
Expand Down
11 changes: 5 additions & 6 deletions src/serviceWorkerRegistration.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint no-console: 0 */

/**
* registerSW
* * register SW
Expand Down Expand Up @@ -57,11 +55,12 @@ export const registerServiceWorker = async (okapiConfig, config, logger) => {
}

// talk to me, goose
if (navigator.serviceWorker.controller) {
logger.log('rtr', 'This page is currently controlled by: ', navigator.serviceWorker.controller);
}
navigator.serviceWorker.oncontrollerchange = () => {
logger.log('rtr', 'This page is now controlled by: ', navigator.serviceWorker.controller);
if (navigator.serviceWorker.controller) {
logger.log('rtr', 'This page is currently controlled by: ', navigator.serviceWorker.controller);
} else {
logger.log('rtr', 'SERVICE WORKER NOT ACTIVE');
}
};
}
};
Expand Down
6 changes: 2 additions & 4 deletions src/serviceWorkerRegistration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,11 @@ describe('registerServiceWorker', () => {

await registerServiceWorker(okapiConfig, config, l);

const oConfig = { source: '@folio/stripes-core', type: 'OKAPI_CONFIG', value: okapiConfig };
const lConfig = { source: '@folio/stripes-core', type: 'LOGGER_CONFIG', value: { categories: config.logCategories } };

expect(sw.postMessage).toHaveBeenNthCalledWith(1, oConfig);
expect(sw.postMessage).toHaveBeenNthCalledWith(2, lConfig);
expect(sw.postMessage).toHaveBeenCalledWith(lConfig);
expect(typeof navigator.serviceWorker.oncontrollerchange).toBe('function');
expect(l.log).toHaveBeenCalledTimes(4);
expect(l.log).toHaveBeenCalledTimes(3);
});
};

Expand Down
2 changes: 1 addition & 1 deletion test/bigtest/tests/login-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ describe('Login', () => {
//
// we'll need to cover these components with jest/RTL tests
// eventually.
describe.skip('with valid credentials', () => {
describe('with valid credentials', () => {
beforeEach(async () => {
const { username, password, submit } = login;

Expand Down
1 change: 1 addition & 0 deletions test/jest/__mock__/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './microStripesConfig.mock';
import './stripesConfig.mock';
import './intl.mock';
import './stripesIcon.mock';
Expand Down
5 changes: 5 additions & 0 deletions test/jest/__mock__/microStripesConfig.mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
jest.mock('micro-stripes-config', () => ({
okapiUrl: 'https://los-alamos-barbie-has-a-nice-tan.oh-wa.it',
okapiTenant: 'kenough',
}),
{ virtual: true });

0 comments on commit 6c6a85e

Please sign in to comment.