diff --git a/offline/install/install.ts b/offline/install/install.ts index db4f61bb32..99b60b98b5 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,7 +1,3 @@ namespace $ { - try { - $mol_offline.main.run() - } catch( error ) { - console.error( error ) - } + $mol_offline.attach_to($mol_worker_service) } diff --git a/offline/offline.ts b/offline/offline.ts index df70a36fd1..309500d174 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,16 +1,21 @@ namespace $ { - export type $mol_offline_web_message = { - ignore_cache?: boolean - blocked_urls?: readonly string[] - cached_urls?: readonly string[] - } - - export class $mol_offline extends $mol_worker { - static main = new $mol_offline + 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.value('ignore_cache', true) + }) + } - blocked(urls?: readonly string[]) { return urls ?? [] } - cached(urls?: readonly string[]) { return urls ?? [] } + override defaults() { + return { + ignore_cache: false, + blocked_urls: [ + '//cse.google.com/adsense/search/async-ads.js' + ] + } + } } } diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 1cc44e5d5e..37f62b7cf8 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -1,77 +1,11 @@ -/// - namespace $ { - export class $mol_offline_web extends $mol_worker_web { - static main = new $mol_offline_web - - override path() { return 'web.js' } - - override ready(reg: $mol_worker_reg_active) { - reg.active.postMessage({ ignore_cache: false }) - } - - override async registration_init() { - window.addEventListener('message', this.window_message.bind(this)) - return super.registration_init() - } - - protected window_message(e: MessageEvent) { - const data = e.data - if (data === 'mol_build_obsolete') return this.send({ ignore_cache: true }) - } - - blocked(urls?: readonly string[]) { - urls = urls ?? this.blocked_urls - this.send({ blocked_urls: urls }) - return urls - } - - cached(urls?: readonly string[]) { - urls = urls ?? this.cached_urls - this.send({ cached_urls: urls }) - return urls - } - - override message(event: ExtendableMessageEvent) { - const data = event.data as string | null | $mol_offline_web_message - if ( ! data || typeof data !== 'object' ) return - - if (data.ignore_cache !== undefined) this.ignore_cache = data.ignore_cache - if (data.blocked_urls) this.blocked_regexp = this.url_regexp(data.blocked_urls) - if (data.cached_urls) this.cached_regexp = this.url_regexp(data.cached_urls) - } - - - override activate(event: ExtendableEvent) { - super.activate(event) - $$.$mol_log3_done({ - place: '$mol_offline', - message: 'Activated', - }) - } - - protected blocked_urls = [ - '//cse\.google\.com/adsense/search/async-ads\.js' - ] as readonly string[] - - protected cached_urls = [ - '.*/index\.html' - ] as readonly string[] - - protected blocked_regexp = this.url_regexp(this.blocked_urls) - protected cached_regexp = this.url_regexp(this.cached_urls) - - url_regexp(list: readonly string[]) { - return new RegExp(`#^https?:(?:(?:${list.join(')|(?:')}))#`) - } - - protected ignore_cache = false + export class $mol_offline_web extends $mol_offline { override fetch_event(event: FetchEvent) { const request = event.request - if( this.blocked_regexp.test(request.url) ) { - return event.respondWith( + if( this.value('blocked_urls')?.includes(request.url.replace( /^https?:/, '' )) ) { + event.respondWith( new Response( null, { @@ -80,23 +14,25 @@ namespace $ { }, ) ) + 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 = this.cached_regexp.test(request.url) + const force_cache = /.+\/[^\/]+\.html/.test(request.url) const no_cache = request.cache === 'no-cache' && ! force_cache - if (this.ignore_cache || request.cache === 'reload' || no_cache) { + if (this.value('ignore_cache') || request.cache === 'reload' || no_cache) { if (request.cache !== 'no-cache' && request.cache !== 'reload') { request = new Request(request, { cache: 'no-cache' }) diff --git a/worker/service/plugin.ts b/worker/service/plugin.ts new file mode 100644 index 0000000000..162bd58cbc --- /dev/null +++ b/worker/service/plugin.ts @@ -0,0 +1,49 @@ +/// +namespace $ { + export class $mol_worker_service_plugin extends $mol_object { + static attach_to< This extends typeof $mol_worker_service_plugin >( + this : This, + worker = $mol_worker_service, + 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 new file mode 100644 index 0000000000..f54dd424f7 --- /dev/null +++ b/worker/service/worker.ts @@ -0,0 +1,26 @@ +namespace $ { + + export class $mol_worker_service extends $mol_object { + static in_worker() { return typeof window === 'undefined' } + + static path() { return 'web.js' } + + 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 }) + } + + return next ?? null + } + + static attach(plugin: $mol_worker_service_plugin) { + this.plugins[plugin.id] = plugin + plugin.data_actual = state => this.data_actual(plugin.id, state) + } + + static send(data: unknown) {} + } +} diff --git a/worker/service/worker.web.ts b/worker/service/worker.web.ts new file mode 100644 index 0000000000..bce4033419 --- /dev/null +++ b/worker/service/worker.web.ts @@ -0,0 +1,164 @@ +namespace $ { + export type $mol_worker_service_reg_active = ServiceWorkerRegistration & { active: ServiceWorker } + export type $mol_worker_service_reg_installing = ServiceWorkerRegistration & { installing: ServiceWorker } + + export class $mol_worker_service_web extends $mol_worker_service { + static is_supported() { + if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) { + console.warn( 'HTTPS or localhost is required for service workers.' ) + return false + } + + if( ! navigator.serviceWorker ) { + console.warn( 'Service Worker is not supported.' ) + return false + } + + return true + } + + protected static registration = null as null | $mol_worker_service_reg_active + + 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 + if (reg.installing) this.installing(reg as $mol_worker_service_reg_installing) + + await ready + + this.registration = reg + } 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 + } + + this.send(intial_state) + + return reg + } + + + static installing(reg: $mol_worker_service_reg_installing) { + reg.addEventListener( 'updatefound', this.update_found.bind(this, reg.installing)) + } + + 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) + } + + static override attach(plugin: $mol_worker_service_plugin) { + super.attach(plugin) + + if ( this.in_worker() ) { + this.worker() + return + } + + 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 + + worker.addEventListener( 'beforeinstallprompt' , this.before_install.bind(this) ) + worker.addEventListener( 'install' , this.install.bind(this)) + worker.addEventListener( 'activate' , this.activate.bind(this)) + 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) + + return worker + } + + static message(event: ExtendableMessageEvent) { + const data = event.data as string | null | { + [k: string]: unknown + } + if ( ! data || typeof data !== 'object' ) return false + + for (let name in data) { + this.data_actual(name, data[name]) + } + + 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 + } + + if (! handled) event.prompt?.() + + return true + } + + static install(event: ExtendableEvent) { + let handled + for (let name in this.plugins) { + if (this.plugins[name]?.install(event)) handled = true + } + if (! handled) this.worker().skipWaiting() + return true + } + + static activate(event: ExtendableEvent) { + let handled + for (let name in this.plugins) { + if (this.plugins[name]?.activate(event)) handled = true + } + + 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 + for (let name in this.plugins) { + if (this.plugins[name]?.fetch_event(event)) handled = true + } + return handled + } + } + + $.$mol_worker_service = $mol_worker_service_web + +} diff --git a/worker/worker.ts b/worker/worker.ts deleted file mode 100644 index 888d33e481..0000000000 --- a/worker/worker.ts +++ /dev/null @@ -1,9 +0,0 @@ -namespace $ { - export class $mol_worker extends $mol_object { - run() { - return false - } - - async send(data: unknown) {} - } -} diff --git a/worker/worker.web.ts b/worker/worker.web.ts deleted file mode 100644 index c6de834f99..0000000000 --- a/worker/worker.web.ts +++ /dev/null @@ -1,126 +0,0 @@ -/// - -namespace $ { - export type $mol_worker_reg_active = ServiceWorkerRegistration & { active: ServiceWorker } - export type $mol_worker_reg_installing = ServiceWorkerRegistration & { installing: ServiceWorker } - - export class $mol_worker_web extends $mol_worker { - path() { return '' } - - in_worker() { return typeof window === 'undefined' } - - is_supported() { - if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) { - console.warn( 'HTTPS or localhost is required for service workers.' ) - return false - } - - if( ! navigator.serviceWorker ) { - console.warn( 'Service Worker is not supported.' ) - return false - } - - return true - } - - protected _registration = null as null | Promise<$mol_worker_reg_active | null> - - registration() { - if (this._registration) return this._registration - if ( this.in_worker() ) return null - if ( ! this.is_supported() ) return null - - return this._registration = this.registration_init() - } - - async registration_init() { - let reg - let ready - - try { - const reg_promise = navigator.serviceWorker.register(this.path()) - ready = navigator.serviceWorker.ready - reg = await reg_promise - } catch (error) { - console.error(error) - return null - } - - if (reg.installing) this.installing(reg as $mol_worker_reg_installing) - - const reg_ready = (await ready) as $mol_worker_reg_active - if (! reg_ready.active) { - throw new Error('ServiceWorkerRegistration is not active') - } - - this.ready(reg_ready) - - return reg_ready - } - - installing(reg: $mol_worker_reg_installing) { - reg.addEventListener( 'updatefound', this.update_found.bind(this, reg.installing)) - } - - update_found(worker: ServiceWorker) { - worker.addEventListener( 'statechange', this.statechange.bind(this, worker)) - } - - statechange(worker: ServiceWorker) {} - - ready(reg: $mol_worker_reg_active) {} - - override async send(data: unknown) { - try { - const reg = await this.registration() - reg?.active.postMessage(data) - } catch (e) { - console.error(e) - } - } - - override run() { - if (this.registration()) return true - this.worker() - return false - } - - _worker = null as null | ServiceWorkerGlobalScope - - worker() { - if (this._worker) return this._worker - const worker = this._worker = self as unknown as ServiceWorkerGlobalScope - - this.worker_init() - - return worker - } - - worker_init() { - const worker = this.worker() - worker.addEventListener( 'beforeinstallprompt' , this.before_install.bind(this) ) - worker.addEventListener( 'install' , this.install.bind(this)) - worker.addEventListener( 'activate' , this.activate.bind(this)) - worker.addEventListener( 'message', this.message.bind(this)) - worker.addEventListener( 'fetch', this.fetch_event.bind(this)) - } - - message(event: ExtendableMessageEvent) { - } - - before_install(event: Event & { prompt?(): void }) { - event.prompt?.() - } - - install(event: ExtendableEvent) { this.worker().skipWaiting() } - - activate(event: ExtendableEvent) { - event.waitUntil( this.worker().clients.claim() ) - } - - fetch_event(event: FetchEvent) {} - } - - $.$mol_worker = $mol_worker_web - -}