From 3593a0ab99643dcd517d208eaeeb759c55222ab6 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 12:55:48 +0300 Subject: [PATCH] $mol_worker refactor --- offline/install/install.ts | 2 +- offline/offline.ts | 73 +++++++++++++++++------ offline/offline.web.ts | 75 ------------------------ worker/service/plugin.ts | 49 ---------------- worker/service/worker.ts | 44 +++++++++++--- worker/service/worker.web.ts | 108 ++++++++++++++--------------------- 6 files changed, 135 insertions(+), 216 deletions(-) delete mode 100644 offline/offline.web.ts delete mode 100644 worker/service/plugin.ts diff --git a/offline/install/install.ts b/offline/install/install.ts index 99b60b98b5..e16848bd33 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,3 +1,3 @@ namespace $ { - $mol_offline.attach_to($mol_worker_service) + $mol_offline.attach() } diff --git a/offline/offline.ts b/offline/offline.ts index 4a28e90db1..d0f264df32 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,28 +1,67 @@ namespace $ { - export class $mol_offline extends $mol_worker_service_plugin { - constructor() { - super() - this.$.$mol_dom_context.addEventListener('message', e => { - if (e.data === 'mol_build_obsolete') this.ignore_cache(this.value('ignore_cache', true)) - }) + export class $mol_offline extends $mol_worker_service { + blocked_urls = [ + '//cse.google.com/adsense/search/async-ads.js' + ] + + override blocked( request: Request ) { + const normalized_url = request.url.replace( /^https?:/, '' ) + + return this.blocked_urls.includes(normalized_url) } - protected ignore_cache(next?: null | boolean) { - return this.$.$mol_state_session.value(`${this}.ignore_cache()`, next) + override modify(request: Request) { + + if( request.method !== 'GET' ) return null + if( !/^https?:/.test( request.url ) ) return null + if( /\?/.test( request.url ) ) return null + if( request.cache === 'no-store' ) return null + + return this.respond(request) } - @ $mol_mem - override defaults() { - const ignore_cache = this.ignore_cache() - this.ignore_cache(null) + async respond(request: Request) { + let fallback_header + + const index_html = /.+\/index\.html/.test(request.url) + + const cache = request.cache + + if (cache === 'reload' || ( cache === 'no-cache' && ! index_html ) ) { + if (cache === 'reload') { + // F5 + Disable cache + request = new Request(request, { cache: 'no-cache' }) + } + + // fetch with fallback to cache if statuses not match + try { + const actual = await fetch(request) + if (actual.status < 400) return actual - return { - ignore_cache: ignore_cache ?? false, - blocked_urls: [ - '//cse.google.com/adsense/search/async-ads.js' - ] + throw new Error( + `${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`, + { cause: actual } + ) + + } catch (err) { + fallback_header = `${(err as Error).cause instanceof Response ? '' : '500 '}${ + (err as Error).message} $mol_offline fallback to cache` + } + } + + if (cache !== 'force-cache') { + request = new Request(request, { cache: 'force-cache' }) } + + const cached = await fetch(request) + + if (! fallback_header || cached.headers.get('$mol_offline_remote_status')) return cached + + const clone = new Response(cached.body, cached) + clone.headers.set( '$mol_offline_remote_status', fallback_header ?? '') + + return clone } } diff --git a/offline/offline.web.ts b/offline/offline.web.ts deleted file mode 100644 index 35d55b873a..0000000000 --- a/offline/offline.web.ts +++ /dev/null @@ -1,75 +0,0 @@ -namespace $ { - export class $mol_offline_web extends $mol_offline { - - override fetch_event(event: FetchEvent) { - const request = event.request - - const blocked_urls = this.value('blocked_urls') - const normalized_url = request.url.replace( /^https?:/, '' ) - - if( blocked_urls?.includes(normalized_url) ) { - event.respondWith( - new Response( - null, - { - status: 418, - statusText: 'Blocked' - }, - ) - ) - return true - } - - if( request.method !== 'GET' ) return - if( !/^https?:/.test( request.url ) ) return - if( /\?/.test( request.url ) ) return - if( request.cache === 'no-store' ) return - - event.respondWith( this.respond(event.request) ) - return true - } - - async respond(request: Request) { - let fallback_header - - const force_cache = /.+\/index\.html/.test(request.url) - const no_cache = request.cache === 'no-cache' && ! force_cache - const ignore_cache = this.value('ignore_cache') - - if (ignore_cache || request.cache === 'reload' || no_cache) { - - if (request.cache !== 'no-cache' && request.cache !== 'reload') { - request = new Request(request, { cache: 'no-cache' }) - } - - // fetch with fallback to cache if statuses not match - try { - const actual = await fetch(request) - if (actual.status < 400) return actual - - throw new Error( - `${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`, - { cause: actual } - ) - - } catch (err) { - fallback_header = `${(err as Error).cause instanceof Response ? '' : '500 '}${ - (err as Error).message} $mol_offline fallback to cache` - } - } - - const cached = await fetch(new Request(request, { cache: 'force-cache' })) - - if (! fallback_header || cached.headers.get('$mol_offline_remote_status')) return cached - - const clone = new Response(cached.body, cached) - clone.headers.set( '$mol_offline_remote_status', fallback_header ?? '') - - return clone - } - - } - - $.$mol_offline = $mol_offline_web - -} diff --git a/worker/service/plugin.ts b/worker/service/plugin.ts deleted file mode 100644 index 735e58d88b..0000000000 --- a/worker/service/plugin.ts +++ /dev/null @@ -1,49 +0,0 @@ -/// -namespace $ { - export class $mol_worker_service_plugin extends $mol_object { - static attach_to< This extends typeof $mol_worker_service_plugin >( - this : This, - worker: { attach(plugin: $mol_worker_service_plugin): void }, - config?: Partial< InstanceType< This > >, - ) { - const plugin = new this - if (config) { - for( let key in config ) ( plugin as any )[ key ] = config[ key ]! - } - - worker.attach(plugin) - - return plugin as InstanceType< This > - } - - defaults() { return {} } - - id = this.toString() - - @ $mol_mem - data_actual( next?: Partial> | null ) { - return next ?? null - } - - @ $mol_mem - data( next?: Partial> | null ) { - return this.data_actual(next) ?? this.defaults() as ReturnType< this['defaults'] > - } - - value< Field extends keyof NonNullable> >( - field: Field, - value?: NonNullable>[ Field ] | null, - ) { - const next = value === undefined ? undefined : { ...this.data(), [ field ]: value } - - return this.data(next)?.[ field as never ] as NonNullable>[ Field ] - } - - init(worker: ServiceWorkerGlobalScope) {} - - fetch_event(event: FetchEvent) { return false as void | boolean } - activate(event: ExtendableEvent) { return false as void | boolean } - install(event: ExtendableEvent) { return false as void | boolean } - before_install(event: Event & { prompt?(): void }) { return false as void | boolean } - } -} diff --git a/worker/service/worker.ts b/worker/service/worker.ts index f54dd424f7..d0d56a90da 100644 --- a/worker/service/worker.ts +++ b/worker/service/worker.ts @@ -5,22 +5,48 @@ namespace $ { static path() { return 'web.js' } - static plugins = {} as Record + static plugins = {} as Record - @ $mol_mem_key - static data_actual(plugin_name: string, next?: State) { - if (next && ! this.in_worker()) { - this.send({ [plugin_name]: next }) - } + static inited = false + static init() {} + static ready() {} + static data(next?: unknown) { return next ?? null } - static attach(plugin: $mol_worker_service_plugin) { + static attach< This extends typeof $mol_worker_service >( + this : This, + config?: Partial< InstanceType< This > >, + ) { + if ( ! this.inited ) { + this.init() + this.inited = true + } + const plugin = this.make(config ?? {}) this.plugins[plugin.id] = plugin - plugin.data_actual = state => this.data_actual(plugin.id, state) + + return plugin } - static send(data: unknown) {} + id = this.toString() + + static blocked_response() { + return new Response( + null, + { + status: 418, + statusText: 'Blocked' + }, + ) + } + + blocked(res: Request) { return false } + modify(res: Request) { return null as null | Response | PromiseLike } + + before_install() {} + install() {} + activate() {} + state_change() {} } } diff --git a/worker/service/worker.web.ts b/worker/service/worker.web.ts index 9f026341a8..e6ab988b2c 100644 --- a/worker/service/worker.web.ts +++ b/worker/service/worker.web.ts @@ -1,3 +1,5 @@ +/// + namespace $ { export type $mol_worker_service_reg_active = ServiceWorkerRegistration & { active: ServiceWorker } export type $mol_worker_service_reg_installing = ServiceWorkerRegistration & { installing: ServiceWorker } @@ -8,7 +10,7 @@ namespace $ { console.warn( 'HTTPS or localhost is required for service workers.' ) return false } - + if( ! navigator.serviceWorker ) { console.warn( 'Service Worker is not supported.' ) return false @@ -19,33 +21,34 @@ namespace $ { protected static registration = null as null | $mol_worker_service_reg_active + static override init() { + if ( this.in_worker() ) { + this.worker() + return + } + + if ( ! this.is_supported() ) return + this.registration_init() + } + static async registration_init() { - let reg - let ready try { const reg_promise = navigator.serviceWorker.register(this.path()) - ready = navigator.serviceWorker.ready as Promise<$mol_worker_service_reg_active> - reg = (await reg_promise) as $mol_worker_service_reg_active + const ready = navigator.serviceWorker.ready as Promise<$mol_worker_service_reg_active> + const reg = (await reg_promise) as $mol_worker_service_reg_active if (reg.installing) this.installing(reg as $mol_worker_service_reg_installing) await ready this.registration = reg + this.ready() + + return true } catch (error) { console.error(error) - return null - } - - let intial_state = {} as Record - for (let name in this.plugins) { - const data = this.plugins[name].data() - intial_state[name] = data + return false } - - this.send(intial_state) - - return reg } static installing(reg: $mol_worker_service_reg_installing) { @@ -53,39 +56,18 @@ namespace $ { } static update_found(worker: ServiceWorker) { - worker.addEventListener( 'statechange', this.statechange.bind(this, worker)) - } - - static statechange(worker: ServiceWorker) {} - - static override send(data: unknown) { - if (! this.registration?.active) { - throw new Error('Send called before worker ready') - } - - this.registration.active.postMessage(data) + worker.addEventListener( 'statechange', this.state_change.bind(this, worker)) } - static override attach(plugin: $mol_worker_service_plugin) { - super.attach(plugin) - - if ( this.in_worker() ) { - this.worker() - return + static state_change(worker: ServiceWorker) { + for (let name in this.plugins) { + this.plugins[name].state_change() } - - if (this.registration) return - if ( ! this.is_supported() ) return - - this.registration_init() } - protected static _worker = null as null | ServiceWorkerGlobalScope - static worker() { - if (this._worker) return this._worker - - const worker = this._worker = self as unknown as ServiceWorkerGlobalScope + const worker = self as unknown as ServiceWorkerGlobalScope + if (this.inited) return worker worker.addEventListener( 'beforeinstallprompt' , this.before_install.bind(this) ) worker.addEventListener( 'install' , this.install.bind(this)) @@ -93,7 +75,7 @@ namespace $ { worker.addEventListener( 'message', this.message.bind(this)) worker.addEventListener( 'fetch', this.fetch_event.bind(this)) - for (let name in this.plugins) this.plugins[name].init(worker) + // for (let name in this.plugins) this.plugins[name].init(worker) return worker } @@ -104,58 +86,54 @@ namespace $ { } if ( ! data || typeof data !== 'object' ) return false - for (let name in data) { - this.data_actual(name, data[name]) - } + this.data(data) return true } static before_install(event: Event & { prompt?(): void }) { - let handled for (let name in this.plugins) { - if (this.plugins[name]?.before_install(event)) handled = true + this.plugins[name].before_install() } - if (! handled) event.prompt?.() - - return true + event.prompt?.() } static install(event: ExtendableEvent) { - let handled for (let name in this.plugins) { - if (this.plugins[name]?.install(event)) handled = true + this.plugins[name].install() } - if (! handled) this.worker().skipWaiting() - return true + this.worker().skipWaiting() } static activate(event: ExtendableEvent) { - let handled for (let name in this.plugins) { - if (this.plugins[name]?.activate(event)) handled = true + this.plugins[name].activate() } - if (handled) return true - event.waitUntil( this.worker().clients.claim() ) this.$.$mol_log3_done({ place: `${this}.activate()`, message: 'Activated', }) - - return true } static fetch_event(event: FetchEvent) { - let handled + const request = event.request + for (let name in this.plugins) { - if (this.plugins[name]?.fetch_event(event)) handled = true + if (this.plugins[name]?.blocked(request)) { + return event.respondWith(this.blocked_response()) + } + } + + for (let name in this.plugins) { + const response = this.plugins[name]?.modify(request) + if (response) return event.respondWith(response) } - return handled } + } $.$mol_worker_service = $mol_worker_service_web