Skip to content
This repository has been archived by the owner on Nov 29, 2023. It is now read-only.

Handle/cache offline-capable mode media requests in service worker #465

Draft
wants to merge 20 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
95ba407
Handle/cache offline-capable mode media requests in service worker
eyelidlessness Oct 5, 2022
c94eef6
Remove outdated tests for offline media
eyelidlessness Oct 6, 2022
d8ebca9
Handle previously cached forms with data-offline-src attributes
eyelidlessness Oct 6, 2022
c58d199
Test applicaiton-cache.js
eyelidlessness Oct 6, 2022
02327cb
Fix: always serve current state of offline worker script
eyelidlessness Oct 8, 2022
61da522
Small service worker fixes
eyelidlessness Oct 8, 2022
7e23a6e
Fix: remove stale versioned caches
eyelidlessness Oct 8, 2022
bff15df
Fix: prefetch is a hint, manually request linked resources
eyelidlessness Oct 8, 2022
afe3ee3
Allow for-of statements in ESLint
eyelidlessness Oct 8, 2022
3491142
Fix: always use the same cache key when requesting the page itself
eyelidlessness Oct 8, 2022
853985c
Preserve HTML caches, reduce network use on load
eyelidlessness Oct 8, 2022
2636925
Exclude build artifacts from ESLint and Prettier
eyelidlessness Oct 15, 2022
4e25bce
Show update banner rather than reload if update is detected after load
eyelidlessness Oct 15, 2022
a1a4207
Document current offline caching and network behavior
eyelidlessness Oct 19, 2022
4e717f8
Document updated offline/caching behavior
eyelidlessness Oct 21, 2022
5de6d66
Remove blocking on initial form cache update
eyelidlessness Oct 21, 2022
10996fb
Show banner/don't reload if service worker update takes >500ms
eyelidlessness Oct 21, 2022
e9263c9
Add comments explaining use of /x/ cache key for HTML
eyelidlessness Oct 21, 2022
a1f6b7a
Clarify CacheStorage Cache API & Cache keys used
eyelidlessness Oct 21, 2022
1c39cec
Fix: don't treat direct requests for non-form HTML URLs as form requests
eyelidlessness Nov 7, 2022
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
205 changes: 205 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
const baseVariablesConfig = require('eslint-config-airbnb-base/rules/variables');
const baseStyleConfig = require('eslint-config-airbnb-base/rules/style');

const serviceWorkerGlobals = baseVariablesConfig.rules[
'no-restricted-globals'
].filter((item) => item.name !== 'self' && item !== 'self');

const baseNoRestrictedSyntax =
baseStyleConfig.rules['no-restricted-syntax'].slice(1);
const noRestrictedSyntax = [
'warn',
...baseNoRestrictedSyntax.filter(
(rule) => rule.selector !== 'ForOfStatement'
),
];

module.exports = {
env: {
es6: true,
browser: true,
node: false,
},
globals: {
Promise: true,
structuredClone: true,
},
extends: ['airbnb', 'prettier'],
plugins: ['chai-friendly', 'jsdoc', 'prettier', 'unicorn'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2021,
},
settings: {
jsdoc: {
tagNamePreference: {
returns: 'return',
},
},
},
ignorePatterns: ['public/js/build/**/*'],
rules: {
'prettier/prettier': 'error',
'import/no-unresolved': [
'error',
{
ignore: [
'enketo/config',
'enketo/widgets',
'enketo/translator',
'enketo/dialog',
'enketo/file-manager',
],
},
],

'react/destructuring-assignment': 'off',

'array-callback-return': 'warn',
'consistent-return': 'warn',
'global-require': 'warn',
'import/order': 'warn',
'import/extensions': 'warn',
'no-param-reassign': 'warn',
'no-plusplus': 'warn',
'no-promise-executor-return': 'warn',
'no-restricted-globals': 'warn',
'no-restricted-syntax': noRestrictedSyntax,
'no-return-assign': 'warn',
'no-shadow': 'warn',
'no-underscore-dangle': 'warn',
'no-unused-expressions': 'warn',
'no-use-before-define': [
'warn',
{
functions: false,
},
],
'prefer-const': 'warn',
'no-cond-assign': 'warn',
'no-nested-ternary': 'warn',
'prefer-destructuring': 'warn',
'import/no-dynamic-require': 'warn',
'prefer-promise-reject-errors': 'warn',
},
overrides: [
{
files: ['**/*.md'],
parser: 'markdown-eslint-parser',
rules: {
'prettier/prettier': ['error', { parser: 'markdown' }],
},
},

{
files: [
'app.js',
'app/**/*.js',
'!app/views/**/*.js',
'tools/redis-repl',
],
env: {
browser: false,
node: true,
},
ecmaFeatures: {
modules: false,
},
},

{
files: [
'Gruntfile.js',
'config/build.js',
'scripts/build.js',
'test/client/config/karma.conf.js',
'test/server/**/*.js',
'tools/**/*.js',
],
env: {
browser: false,
node: true,
},
ecmaFeatures: {
modules: false,
},
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
},
},

{
files: [
'app/views/**/*.js',
'public/js/src/**/*.js',
'test/client/**/*.js',
],
env: {
browser: true,
node: false,
},
},

{
files: ['public/js/src/**/*.js'],
globals: {
ENV: true,
},
},

{
files: ['public/js/src/module/offline-app-worker.js'],
globals: {
self: true,
},
rules: {
'no-restricted-globals': serviceWorkerGlobals,
},
},

{
files: ['test/client/**/*.js'],
env: {
mocha: true,
},
globals: {
expect: true,
sinon: true,
},
rules: {
'no-console': 0,
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
},
},

{
files: ['test/server/**/*.js'],
env: {
mocha: true,
},
globals: {
expect: true,
sinon: true,
},
rules: {
'no-console': 0,
},
},

{
files: ['**/*.mjs'],
parser: '@babel/eslint-parser',
parserOptions: {
sourceType: 'module',
ecmaVersion: 2021,
requireConfigFile: false,
},
},
],
};
3 changes: 1 addition & 2 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Ignore artifacts:

.nyc*
public/js/build/*
*/offline-app-worker-partial.js
public/js/build/**/*
*/node_modules/*
docs/*
test-coverage/*
Expand Down
2 changes: 1 addition & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ module.exports = (grunt) => {
'find locales -name "translation-combined.json" -delete && rm -fr locales/??',
},
'clean-js': {
command: 'rm -f public/js/build/* && rm -f public/js/*.js',
command: 'rm -rf public/js/build/* && rm -f public/js/*.js',
},
translation: {
command:
Expand Down
77 changes: 21 additions & 56 deletions app/controllers/offline-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
*/

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const express = require('express');

const router = express.Router();
Expand All @@ -15,6 +13,7 @@ const config = require('../models/config-model').server;
module.exports = (app) => {
app.use(`${app.get('base path')}/`, router);
};

router.get('/x/offline-app-worker.js', (req, res, next) => {
if (config['offline enabled'] === false) {
const error = new Error(
Expand All @@ -23,59 +22,25 @@ router.get('/x/offline-app-worker.js', (req, res, next) => {
error.status = 404;
next(error);
} else {
res.set('Content-Type', 'text/javascript').send(getScriptContent());
// We add as few explicit resources as possible because the offline-app-worker can do this dynamically and that is preferred
// for easier maintenance of the offline launch feature.
const resources = config['themes supported']
.flatMap((theme) => [
`${config['base path']}${config['offline path']}/css/theme-${theme}.css`,
`${config['base path']}${config['offline path']}/css/theme-${theme}.print.css`,
])
.concat([
`${config['base path']}${config['offline path']}/images/icon_180x180.png`,
]);

const link = resources
.map((resource) => `<${resource}>; rel="prefetch"`)
.join(', ');

const script = fs.readFileSync(config.offlineWorkerPath, 'utf-8');

res.set('Content-Type', 'text/javascript');
res.set('Link', link);
res.send(script);
}
});

/**
* Assembles script contentå
*/
function getScriptContent() {
// Determining hash every time, is done to make development less painful (refreshing service worker)
// The partialScriptHash is not actually required but useful to see which offline-app-worker-partial.js is used during troubleshooting.
lognaturel marked this conversation as resolved.
Show resolved Hide resolved
// by going to http://localhost:8005/x/offline-app-worker.js and comparing the version with the version shown in the side slider of the webform.
const partialOfflineAppWorkerScript = fs.readFileSync(
path.resolve(
config.root,
'public/js/src/module/offline-app-worker-partial.js'
),
'utf8'
);
const partialScriptHash = crypto
.createHash('md5')
.update(partialOfflineAppWorkerScript)
.digest('hex')
.substring(0, 7);
const configurationHash = crypto
.createHash('md5')
.update(JSON.stringify(config))
.digest('hex')
.substring(0, 7);
const version = [config.version, configurationHash, partialScriptHash].join(
'-'
);
// We add as few explicit resources as possible because the offline-app-worker can do this dynamically and that is preferred
// for easier maintenance of the offline launch feature.
const resources = config['themes supported']
.reduce((accumulator, theme) => {
accumulator.push(
`${config['base path']}${config['offline path']}/css/theme-${theme}.css`
);
accumulator.push(
`${config['base path']}${config['offline path']}/css/theme-${theme}.print.css`
);

return accumulator;
}, [])
.concat([
`${config['base path']}${config['offline path']}/images/icon_180x180.png`,
]);

return `
const version = '${version}';
const resources = [
'${resources.join("',\n '")}'
];

${partialOfflineAppWorkerScript}`;
}
6 changes: 6 additions & 0 deletions app/models/config-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,11 @@ if (config['id length'] < 4) {
config['id length'] = 31;
}

config.offlineWorkerPath = path.resolve(
config.root,
'public/js/build/module/offline-app-worker.js'
);

module.exports = {
/**
* @type { object }
Expand Down Expand Up @@ -371,6 +376,7 @@ module.exports = {
csrfCookieName: config['csrf cookie name'],
excludeNonRelevant: config['exclude non-relevant'],
experimentalOptimizations: config['experimental optimizations'],
version: config.version,
},
getThemesSupported,
};
10 changes: 6 additions & 4 deletions config/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ const pkg = require('../package.json');
const cwd = process.cwd();

const entryPoints = pkg.entries.map((entry) => path.resolve(cwd, entry));

const isProduction = process.env.NODE_ENV === 'production';
const { NODE_ENV } = process.env;

module.exports = {
bundle: true,
define: {
ENV: JSON.stringify(NODE_ENV ?? 'production'),
},
entryPoints,
format: 'iife',
minify: isProduction,
minify: true,
outdir: path.resolve(cwd, './public/js/build'),
plugins: [
alias(
Expand All @@ -24,6 +26,6 @@ module.exports = {
)
),
],
sourcemap: isProduction ? false : 'inline',
sourcemap: NODE_ENV === 'production' ? false : 'inline',
target: ['chrome89', 'edge89', 'firefox90', 'safari13'],
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"esbuild": "^0.12.29",
"esbuild-plugin-alias": "^0.1.2",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-import": "^2.26.0",
Expand Down Expand Up @@ -150,7 +151,8 @@
"public/js/src/enketo-webform.js",
"public/js/src/enketo-webform-edit.js",
"public/js/src/enketo-webform-view.js",
"public/js/src/enketo-offline-fallback.js"
"public/js/src/enketo-offline-fallback.js",
"public/js/src/module/offline-app-worker.js"
],
"volta": {
"node": "16.6.1",
Expand Down
Loading