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
-
-}