From b22b11a215c273716aaea94c8386423b1fa93474 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Fri, 25 Oct 2024 15:44:11 +0300 Subject: [PATCH 01/60] $mol_offline refactor, disable cache on page reload via dev server --- build/client/client.js | 1 + offline/install/install.ts | 2 +- offline/offline.ts | 6 +- offline/offline.web.ts | 293 ++++++++++++++++++++++++------------- 4 files changed, 199 insertions(+), 103 deletions(-) diff --git a/build/client/client.js b/build/client/client.js index 22d91b68efe..579932a54ed 100644 --- a/build/client/client.js +++ b/build/client/client.js @@ -10,6 +10,7 @@ function $mol_build_client() { socket.onmessage = message => { if( message.data !== '$mol_build_obsolete' ) return + sessionStorage.setItem('$mol_build_obsolete', '1') location.reload() } diff --git a/offline/install/install.ts b/offline/install/install.ts index b6ce77c459b..dc75de4f4e9 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,6 +1,6 @@ namespace $ { try { - $mol_offline() + new $mol_offline().run() } catch( error ) { console.error( error ) } diff --git a/offline/offline.ts b/offline/offline.ts index 179d65834e7..792ee96b4e8 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,5 +1,9 @@ namespace $ { - export function $mol_offline( ) {} + export class $mol_offline { + run() { + return false + } + } } diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 2b1d160cc46..3ee2008a96a 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -1,109 +1,200 @@ namespace $ { - - const blacklist = new Set([ - '//cse.google.com/adsense/search/async-ads.js' - ]) - - /** Installs service worker proxy, which caches all requests and respond from cache on http errors. */ - export function $mol_offline_web() { - - if( typeof window === 'undefined' ) { + + interface FetchEvent extends Event { + request: Request + respondWith(response: Response | Promise | null): void + waitUntil(promise: Promise): void + } + + export class $mol_offline_web extends $mol_offline { + web_js() { return 'web.js' } + + blacklist = new Set([ + '//cse.google.com/adsense/search/async-ads.js' + ]) + + obsolete_key() { return '$mol_build_obsolete' } + + 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 + } - self.addEventListener( 'install' , ( event : any )=> { - ;( self as any ).skipWaiting() - } ) - - self.addEventListener( 'activate' , ( event : any )=> { - - // caches.delete( '$mol_offline' ) - - ;( self as any ).clients.claim() - - $$.$mol_log3_done({ - place: '$mol_offline', - message: 'Activated', - }) - - } ) - - self.addEventListener( 'fetch' , ( event : any )=> { - - const request = event.request as Request - - if( blacklist.has( request.url.replace( /^https?:/, '' ) ) ) { - return event.respondWith( - new Response( - null, - { - status: 418, - statusText: 'Blocked' - }, - ) - ) - } - - if( request.method !== 'GET' ) return - if( !/^https?:/.test( request.url ) ) return - if( /\?/.test( request.url ) ) return - if( request.cache === 'no-store' ) return - - const fetch_data = () => fetch( request ).then( response => { - if (response.status !== 200) return response - event.waitUntil( - caches.open( '$mol_offline' ).then( - cache => cache.put( request , response ) - ) - ) - - return response.clone() - } ) - - const fresh = request.cache === 'force-cache' ? null : fetch_data() - - if (fresh) event.waitUntil( fresh ) - - event.respondWith( - caches.match( request ).then( - cached => request.cache === 'no-cache' || request.cache === 'reload' - ? ( cached - ? fresh! - .then(actual => { - if (actual.status === cached.status) return actual - throw new Error( - `${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`, - { cause: actual } - ) - }) - .catch((err: Error) => { - const cloned = cached.clone() - const message = `${err.cause instanceof Response ? '' : '500 '}${err.message} $mol_offline fallback to cache` - cloned.headers.set( '$mol_offline_remote_status', message ) - return cloned - }) - : fresh - ) - : ( cached || fresh || fetch_data() ) + if( ! navigator.serviceWorker ) { + console.warn( 'Service Worker is not supported.' ) + return false + } + + return true + } + + protected _registration = null as null | Promise + + registration() { + if (this._registration) return this._registration + if ( this.in_worker() ) return null + if ( ! this.is_supported() ) return null + + navigator.serviceWorker.register(this.web_js()) + + return this._registration = navigator.serviceWorker.ready + } + + async notify() { + const reg = await this.registration() + + const key = this.obsolete_key() + const ignore_cache = sessionStorage.getItem(key) + sessionStorage.removeItem(key) + + if (ignore_cache) reg?.active?.postMessage({ message: key }) + } + + override run() { + if (! this.registration()) { + this.worker() + return false + } + + this.notify() + + // const reg = await this.registration() + // reg?.addEventListener( 'updatefound', ()=> { + // const worker = reg.installing! + // worker.addEventListener( 'statechange', ()=> { + // if( worker.state !== 'activated' ) return + // window.location.reload() + // } ) + // } ) + + return true + } + + _worker = null as null | { + skipWaiting(): void + clients: { + claim(): void + } + addEventListener(name: string, cb: (e: Event) => void): void + } + + worker() { + if (this._worker) return this._worker + const worker = this._worker = self as unknown as NonNullable + worker.addEventListener( 'beforeinstallprompt' , this.beforeinstallprompt.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) as any) + + return worker + } + + message(event: unknown) { + if (! event || typeof event !== 'object') return + + const message = (event as { data: { message?: string }}).data?.message ?? '' + + if (! message) return + if (message === this.obsolete_key()) return this.build_obsolete() + + } + + beforeinstallprompt(event: Event & { prompt?(): void }) { + event.prompt?.() + } + + install(event: Event) { this.worker().skipWaiting() } + + activate(event: Event) { + // caches.delete( '$mol_offline' ) + + this.worker().clients.claim() + + $$.$mol_log3_done({ + place: '$mol_offline', + message: 'Activated', + }) + } + + protected ignore_cache = false + + build_obsolete() { this.ignore_cache = true } + + fetch_event(event: FetchEvent) { + const request = event.request + + if( this.blacklist.has( request.url.replace( /^https?:/, '' ) ) ) { + return event.respondWith( + new Response( + null, + { + status: 418, + statusText: 'Blocked' + }, ) ) - - }) + } + + if( request.method !== 'GET' ) return + if( !/^https?:/.test( request.url ) ) return + if( /\?/.test( request.url ) ) return + if( request.cache === 'no-store' ) return + + const response = this.respond(event) + event.waitUntil( response ) + event.respondWith( response ) + } + + async respond(event: FetchEvent) { + const request = event.request + const fresh = request.cache === 'force-cache' ? null : this.fetch_data(event) + + const cached = this.ignore_cache ? null : await caches.match( request ) + + if (request.cache !== 'no-cache' && request.cache !== 'reload') { + return cached || fresh || this.fetch_data(event) + } + + if ( ! cached || ! fresh) return fresh + + try { + const actual = await fresh + if (actual.status === cached.status) return actual + + throw new Error( + `${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`, + { cause: actual } + ) + + } catch (err) { + const cloned = cached.clone() + const message = `${(err as Error).cause instanceof Response ? '' : '500 '}${ + (err as Error).message} $mol_offline fallback to cache` + + cloned.headers.set( '$mol_offline_remote_status', message ) + + return cloned + } + } + + async put_cache(request: Request, response: Response | Promise) { + const cache = await caches.open( '$mol_offline' ) + return cache.put( request , await response ) + } + + async fetch_data (event: FetchEvent) { + const request = event.request + const response = await fetch( request ) + if (response.status !== 200) return response + + const cached = this.put_cache(request, response) + event.waitUntil(cached) - self.addEventListener( 'beforeinstallprompt' , ( event : any )=> event.prompt() ) - - } else if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) { - console.warn( 'HTTPS or localhost is required for service workers.' ) - } else if( !navigator.serviceWorker ) { - console.warn( 'Service Worker is not supported.' ) - } else { - navigator.serviceWorker.register( 'web.js' ).then( reg => { - // reg.addEventListener( 'updatefound', ()=> { - // const worker = reg.installing! - // worker.addEventListener( 'statechange', ()=> { - // if( worker.state !== 'activated' ) return - // window.location.reload() - // } ) - // } ) - } ) + return response.clone() } } From 9d5f058c69df1675beffd181d9888449580f1475 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Fri, 25 Oct 2024 16:11:31 +0300 Subject: [PATCH 02/60] $mol_offline refactor --- offline/offline.web.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 3ee2008a96a..1cc49e95cd5 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -151,18 +151,18 @@ namespace $ { async respond(event: FetchEvent) { const request = event.request - const fresh = request.cache === 'force-cache' ? null : this.fetch_data(event) + const fresh = request.cache === 'force-cache' ? null : this.fetch_and_cache(event) const cached = this.ignore_cache ? null : await caches.match( request ) if (request.cache !== 'no-cache' && request.cache !== 'reload') { - return cached || fresh || this.fetch_data(event) + return cached || fresh || this.fetch_and_cache(event) } - if ( ! cached || ! fresh) return fresh + if ( ! cached) return fresh try { - const actual = await fresh + const actual = await fresh! if (actual.status === cached.status) return actual throw new Error( @@ -181,20 +181,20 @@ namespace $ { } } - async put_cache(request: Request, response: Response | Promise) { + async put_cache(request: Request, response: Response) { const cache = await caches.open( '$mol_offline' ) - return cache.put( request , await response ) + return cache.put( request , response ) } - async fetch_data (event: FetchEvent) { + async fetch_and_cache (event: FetchEvent) { const request = event.request const response = await fetch( request ) if (response.status !== 200) return response - const cached = this.put_cache(request, response) + const cached = this.put_cache(request, response.clone()) event.waitUntil(cached) - return response.clone() + return response } } From ce606ba4b645f9b80025eacf8ea4556bc810ed9e Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Fri, 25 Oct 2024 16:29:19 +0300 Subject: [PATCH 03/60] $mol_offline refactor 2 - simplify respond --- offline/offline.web.ts | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 1cc49e95cd5..81bfdab9253 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -151,34 +151,34 @@ namespace $ { async respond(event: FetchEvent) { const request = event.request - const fresh = request.cache === 'force-cache' ? null : this.fetch_and_cache(event) - const cached = this.ignore_cache ? null : await caches.match( request ) - if (request.cache !== 'no-cache' && request.cache !== 'reload') { - return cached || fresh || this.fetch_and_cache(event) - } + if ( ! cached) return this.fetch_and_cache(event) - if ( ! cached) return fresh + if (request.cache === 'force-cache') return cached - try { - const actual = await fresh! - if (actual.status === cached.status) return actual + if (request.cache === 'no-cache' || request.cache === 'reload') { + try { + const actual = await this.fetch_and_cache(event) + if (actual.status === cached.status) return actual - throw new Error( - `${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`, - { cause: actual } - ) + throw new Error( + `${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`, + { cause: actual } + ) - } catch (err) { - const cloned = cached.clone() - const message = `${(err as Error).cause instanceof Response ? '' : '500 '}${ - (err as Error).message} $mol_offline fallback to cache` + } catch (err) { + const cloned = cached.clone() + const message = `${(err as Error).cause instanceof Response ? '' : '500 '}${ + (err as Error).message} $mol_offline fallback to cache` - cloned.headers.set( '$mol_offline_remote_status', message ) + cloned.headers.set( '$mol_offline_remote_status', message ) - return cloned + return cloned + } } + + return cached } async put_cache(request: Request, response: Response) { From ca3be5d41bde1cad1edd02cb5b77cc14021c0f10 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Fri, 25 Oct 2024 16:53:47 +0300 Subject: [PATCH 04/60] $mol_offline refactor 3 - simplify respond --- offline/offline.web.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 81bfdab9253..e0ce8c0ea97 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -151,13 +151,14 @@ namespace $ { async respond(event: FetchEvent) { const request = event.request - const cached = this.ignore_cache ? null : await caches.match( request ) + const cached = await caches.match( request ) if ( ! cached) return this.fetch_and_cache(event) if (request.cache === 'force-cache') return cached - if (request.cache === 'no-cache' || request.cache === 'reload') { + if (this.ignore_cache || request.cache === 'no-cache' || request.cache === 'reload') { + // fetch with fallback to cache if statuses not match try { const actual = await this.fetch_and_cache(event) if (actual.status === cached.status) return actual From f965597517902d78c615abe12a085e5a9a49403b Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Fri, 25 Oct 2024 16:59:55 +0300 Subject: [PATCH 05/60] $mol_offline refactor 4 - extracted send --- offline/offline.web.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index e0ce8c0ea97..d037d213f71 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -43,14 +43,21 @@ namespace $ { return this._registration = navigator.serviceWorker.ready } - async notify() { - const reg = await this.registration() + async send(data: { message: string }) { + try { + const reg = await this.registration() + reg?.active?.postMessage(data) + } catch (e) { + console.error(e) + } + } + notify() { const key = this.obsolete_key() const ignore_cache = sessionStorage.getItem(key) sessionStorage.removeItem(key) - if (ignore_cache) reg?.active?.postMessage({ message: key }) + if (ignore_cache) this.send({ message: key }) } override run() { From 81a9ac6078538538c88c79e3061a71f99e0c5f9f Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 26 Oct 2024 00:58:17 +0300 Subject: [PATCH 06/60] $mol_offline refactor --- offline/offline.web.ts | 53 ++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index d037d213f71..0181e45552c 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -1,11 +1,6 @@ -namespace $ { - - interface FetchEvent extends Event { - request: Request - respondWith(response: Response | Promise | null): void - waitUntil(promise: Promise): void - } +/// +namespace $ { export class $mol_offline_web extends $mol_offline { web_js() { return 'web.js' } @@ -80,31 +75,25 @@ namespace $ { return true } - _worker = null as null | { - skipWaiting(): void - clients: { - claim(): void - } - addEventListener(name: string, cb: (e: Event) => void): void - } + _worker = null as null | ServiceWorkerGlobalScope worker() { if (this._worker) return this._worker - const worker = this._worker = self as unknown as NonNullable + const worker = this._worker = self as unknown as ServiceWorkerGlobalScope + // as unknown as NonNullable worker.addEventListener( 'beforeinstallprompt' , this.beforeinstallprompt.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) as any) + worker.addEventListener( 'fetch', this.fetch_event.bind(this)) return worker } - message(event: unknown) { - if (! event || typeof event !== 'object') return - - const message = (event as { data: { message?: string }}).data?.message ?? '' - + message(event: ExtendableMessageEvent) { + const data = event.data + if (! data || typeof data !== 'object') return + const message = (data as { message?: string }).message ?? '' if (! message) return if (message === this.obsolete_key()) return this.build_obsolete() @@ -114,9 +103,9 @@ namespace $ { event.prompt?.() } - install(event: Event) { this.worker().skipWaiting() } + install(event: ExtendableEvent) { this.worker().skipWaiting() } - activate(event: Event) { + activate(event: ExtendableEvent) { // caches.delete( '$mol_offline' ) this.worker().clients.claim() @@ -158,7 +147,12 @@ namespace $ { async respond(event: FetchEvent) { const request = event.request - const cached = await caches.match( request ) + let cached + try { + cached = await caches.match( request ) + } catch (e) { + console.error(e) + } if ( ! cached) return this.fetch_and_cache(event) @@ -176,10 +170,10 @@ namespace $ { ) } catch (err) { - const cloned = cached.clone() const message = `${(err as Error).cause instanceof Response ? '' : '500 '}${ (err as Error).message} $mol_offline fallback to cache` + const cloned = cached.clone() cloned.headers.set( '$mol_offline_remote_status', message ) return cloned @@ -194,13 +188,12 @@ namespace $ { return cache.put( request , response ) } - async fetch_and_cache (event: FetchEvent) { + async fetch_and_cache(event: FetchEvent) { const request = event.request const response = await fetch( request ) - if (response.status !== 200) return response - - const cached = this.put_cache(request, response.clone()) - event.waitUntil(cached) + if (response.status === 200) { + event.waitUntil(this.put_cache(request, response.clone())) + } return response } From c83d213b7bfeaea10956eadd67ae4df69f2d8d11 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 26 Oct 2024 01:56:10 +0300 Subject: [PATCH 07/60] $mol_offline mol_build_obsolete message pass to worker to disable cache --- build/client/client.js | 2 +- offline/offline.web.ts | 29 ++++++++--------------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/build/client/client.js b/build/client/client.js index 579932a54ed..3f4ab24b9b7 100644 --- a/build/client/client.js +++ b/build/client/client.js @@ -10,7 +10,7 @@ function $mol_build_client() { socket.onmessage = message => { if( message.data !== '$mol_build_obsolete' ) return - sessionStorage.setItem('$mol_build_obsolete', '1') + postMessage('mol_build_obsolete') location.reload() } diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 0181e45552c..a81941dd5b8 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -8,8 +8,6 @@ namespace $ { '//cse.google.com/adsense/search/async-ads.js' ]) - obsolete_key() { return '$mol_build_obsolete' } - in_worker() { return typeof window === 'undefined' } is_supported() { @@ -33,12 +31,18 @@ namespace $ { if ( this.in_worker() ) return null if ( ! this.is_supported() ) return null + window.addEventListener('message', this.window_message.bind(this)) + navigator.serviceWorker.register(this.web_js()) return this._registration = navigator.serviceWorker.ready } - async send(data: { message: string }) { + window_message(e: MessageEvent) { + if (e.data === 'mol_build_obsolete') return this.send(e.data) + } + + async send(data: unknown) { try { const reg = await this.registration() reg?.active?.postMessage(data) @@ -47,22 +51,12 @@ namespace $ { } } - notify() { - const key = this.obsolete_key() - const ignore_cache = sessionStorage.getItem(key) - sessionStorage.removeItem(key) - - if (ignore_cache) this.send({ message: key }) - } - override run() { if (! this.registration()) { this.worker() return false } - this.notify() - // const reg = await this.registration() // reg?.addEventListener( 'updatefound', ()=> { // const worker = reg.installing! @@ -91,12 +85,7 @@ namespace $ { } message(event: ExtendableMessageEvent) { - const data = event.data - if (! data || typeof data !== 'object') return - const message = (data as { message?: string }).message ?? '' - if (! message) return - if (message === this.obsolete_key()) return this.build_obsolete() - + if (event.data === 'mol_build_obsolete') this.ignore_cache = true } beforeinstallprompt(event: Event & { prompt?(): void }) { @@ -118,8 +107,6 @@ namespace $ { protected ignore_cache = false - build_obsolete() { this.ignore_cache = true } - fetch_event(event: FetchEvent) { const request = event.request From 9f3722bf62dbc17fd33239870ec87d9dcad34635 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 26 Oct 2024 11:47:23 +0300 Subject: [PATCH 08/60] $mol_offline review fixes --- offline/offline.web.ts | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index a81941dd5b8..ef0383b8ce3 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -75,7 +75,7 @@ namespace $ { if (this._worker) return this._worker const worker = this._worker = self as unknown as ServiceWorkerGlobalScope // as unknown as NonNullable - worker.addEventListener( 'beforeinstallprompt' , this.beforeinstallprompt.bind(this) ) + 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)) @@ -88,7 +88,7 @@ namespace $ { if (event.data === 'mol_build_obsolete') this.ignore_cache = true } - beforeinstallprompt(event: Event & { prompt?(): void }) { + before_install(event: Event & { prompt?(): void }) { event.prompt?.() } @@ -134,22 +134,12 @@ namespace $ { async respond(event: FetchEvent) { const request = event.request - let cached - try { - cached = await caches.match( request ) - } catch (e) { - console.error(e) - } - - if ( ! cached) return this.fetch_and_cache(event) - - if (request.cache === 'force-cache') return cached - + let fallback_header if (this.ignore_cache || request.cache === 'no-cache' || request.cache === 'reload') { // fetch with fallback to cache if statuses not match try { const actual = await this.fetch_and_cache(event) - if (actual.status === cached.status) return actual + if (actual.status < 400) return actual throw new Error( `${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`, @@ -157,14 +147,22 @@ namespace $ { ) } catch (err) { - const message = `${(err as Error).cause instanceof Response ? '' : '500 '}${ + fallback_header = `${(err as Error).cause instanceof Response ? '' : '500 '}${ (err as Error).message} $mol_offline fallback to cache` + } + } - const cloned = cached.clone() - cloned.headers.set( '$mol_offline_remote_status', message ) + let cached + try { + cached = await caches.match( request ) + } catch (e) { + console.error(e) + } - return cloned - } + if ( ! cached) return this.fetch_and_cache(event) + if (fallback_header) { + cached = cached.clone() + cached.headers.set( '$mol_offline_remote_status', fallback_header ) } return cached From 84c0253b851ea116d8134396b56b0bc8c6702d32 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 26 Oct 2024 12:55:21 +0300 Subject: [PATCH 09/60] $mol_offline review fixes 2 --- offline/offline.web.ts | 53 ++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index ef0383b8ce3..7c54b0817dd 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -1,10 +1,15 @@ /// namespace $ { + export type $mol_offline_web_message = { + ignore_cache?: boolean + blacklist?: readonly string[] + } + export class $mol_offline_web extends $mol_offline { web_js() { return 'web.js' } - blacklist = new Set([ + protected blacklist = new Set([ '//cse.google.com/adsense/search/async-ads.js' ]) @@ -39,10 +44,13 @@ namespace $ { } window_message(e: MessageEvent) { - if (e.data === 'mol_build_obsolete') return this.send(e.data) + const data = typeof e.data === 'object' ? e.data : null + if (data === 'mol_build_obsolete') return this.send({ ignore_cache: true }) + if (! data || typeof data !== 'object' || ! ( 'offline_message' in data ) ) return null + this.send(data.offline_message as $mol_offline_web_message) } - async send(data: unknown) { + async send(data: $mol_offline_web_message) { try { const reg = await this.registration() reg?.active?.postMessage(data) @@ -74,7 +82,7 @@ namespace $ { worker() { if (this._worker) return this._worker const worker = this._worker = self as unknown as ServiceWorkerGlobalScope - // as unknown as NonNullable + worker.addEventListener( 'beforeinstallprompt' , this.before_install.bind(this) ) worker.addEventListener( 'install' , this.install.bind(this)) worker.addEventListener( 'activate' , this.activate.bind(this)) @@ -85,7 +93,11 @@ namespace $ { } message(event: ExtendableMessageEvent) { - if (event.data === 'mol_build_obsolete') this.ignore_cache = true + 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.blacklist) this.blacklist = new Set(data.blacklist) } before_install(event: Event & { prompt?(): void }) { @@ -97,8 +109,8 @@ namespace $ { activate(event: ExtendableEvent) { // caches.delete( '$mol_offline' ) - this.worker().clients.claim() - + event.waitUntil( this.worker().clients.claim() ) + $$.$mol_log3_done({ place: '$mol_offline', message: 'Activated', @@ -127,18 +139,15 @@ namespace $ { if( /\?/.test( request.url ) ) return if( request.cache === 'no-store' ) return - const response = this.respond(event) - event.waitUntil( response ) - event.respondWith( response ) + event.respondWith( this.respond(event.request) ) } - async respond(event: FetchEvent) { - const request = event.request + async respond(request: Request) { let fallback_header if (this.ignore_cache || request.cache === 'no-cache' || request.cache === 'reload') { // fetch with fallback to cache if statuses not match try { - const actual = await this.fetch_and_cache(event) + const actual = await this.fetch_and_cache(request) if (actual.status < 400) return actual throw new Error( @@ -159,7 +168,7 @@ namespace $ { console.error(e) } - if ( ! cached) return this.fetch_and_cache(event) + if ( ! cached) return this.fetch_and_cache(request) if (fallback_header) { cached = cached.clone() cached.headers.set( '$mol_offline_remote_status', fallback_header ) @@ -168,16 +177,16 @@ namespace $ { return cached } - async put_cache(request: Request, response: Response) { - const cache = await caches.open( '$mol_offline' ) - return cache.put( request , response ) - } + cache() { return caches.open( '$mol_offline' ) } - async fetch_and_cache(event: FetchEvent) { - const request = event.request + async fetch_and_cache(request: Request) { const response = await fetch( request ) - if (response.status === 200) { - event.waitUntil(this.put_cache(request, response.clone())) + if (response.status !== 200) return response + + try { + await (await this.cache()).put( request , response.clone() ) + } catch (e) { + console.error(e) } return response From c476bb3aac960abb8eb630b9c0ec1700fbef807e Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 26 Oct 2024 13:10:47 +0300 Subject: [PATCH 10/60] $mol_offline fixes 3 --- offline/offline.ts | 7 +++++++ offline/offline.web.ts | 25 ++++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/offline/offline.ts b/offline/offline.ts index 792ee96b4e8..c2a9e8b9252 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,6 +1,13 @@ namespace $ { + export type $mol_offline_web_message = { + ignore_cache?: boolean + blacklist?: readonly string[] + } + export class $mol_offline { + async send(data: $mol_offline_web_message) {} + run() { return false } diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 7c54b0817dd..9037ea4f19b 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -1,11 +1,6 @@ /// namespace $ { - export type $mol_offline_web_message = { - ignore_cache?: boolean - blacklist?: readonly string[] - } - export class $mol_offline_web extends $mol_offline { web_js() { return 'web.js' } @@ -50,7 +45,7 @@ namespace $ { this.send(data.offline_message as $mol_offline_web_message) } - async send(data: $mol_offline_web_message) { + override async send(data: $mol_offline_web_message) { try { const reg = await this.registration() reg?.active?.postMessage(data) @@ -144,10 +139,13 @@ namespace $ { async respond(request: Request) { let fallback_header + let response + if (this.ignore_cache || request.cache === 'no-cache' || request.cache === 'reload') { // fetch with fallback to cache if statuses not match try { - const actual = await this.fetch_and_cache(request) + response = this.fetch_and_cache(request) + const actual = await response if (actual.status < 400) return actual throw new Error( @@ -168,13 +166,14 @@ namespace $ { console.error(e) } - if ( ! cached) return this.fetch_and_cache(request) - if (fallback_header) { - cached = cached.clone() - cached.headers.set( '$mol_offline_remote_status', fallback_header ) - } + if (! cached) return response ?? this.fetch_and_cache(request) + + if (! fallback_header) return cached + + const clone = cached.clone() + clone.headers.set( '$mol_offline_remote_status', fallback_header ) - return cached + return clone } cache() { return caches.open( '$mol_offline' ) } From f8eab6a71e223e139cb7f2540489b456c5c04d11 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 26 Oct 2024 18:05:20 +0300 Subject: [PATCH 11/60] $mol_offline refactor, extract $mol_worker --- offline/offline.ts | 4 +- offline/offline.web.ts | 94 ++++-------------------------- worker/worker.ts | 7 +++ worker/worker.web.ts | 126 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 86 deletions(-) create mode 100644 worker/worker.ts create mode 100644 worker/worker.web.ts diff --git a/offline/offline.ts b/offline/offline.ts index c2a9e8b9252..853d69ba34e 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -5,9 +5,7 @@ namespace $ { blacklist?: readonly string[] } - export class $mol_offline { - async send(data: $mol_offline_web_message) {} - + export class $mol_offline extends $mol_object { run() { return false } diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 9037ea4f19b..727859406f2 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -1,93 +1,30 @@ /// namespace $ { - export class $mol_offline_web extends $mol_offline { - web_js() { return 'web.js' } + export class $mol_offline_web extends $mol_worker_web { + override path() { return 'web.js' } protected blacklist = new Set([ '//cse.google.com/adsense/search/async-ads.js' ]) - 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 + override ready(reg: $mol_worker_reg_active) { + reg.active.postMessage({ ignore_cache: false }) } - protected _registration = null as null | Promise - - registration() { - if (this._registration) return this._registration - if ( this.in_worker() ) return null - if ( ! this.is_supported() ) return null - + override async registration_init() { window.addEventListener('message', this.window_message.bind(this)) - - navigator.serviceWorker.register(this.web_js()) - - return this._registration = navigator.serviceWorker.ready + return super.registration_init() } - window_message(e: MessageEvent) { + protected window_message(e: MessageEvent) { const data = typeof e.data === 'object' ? e.data : null if (data === 'mol_build_obsolete') return this.send({ ignore_cache: true }) if (! data || typeof data !== 'object' || ! ( 'offline_message' in data ) ) return null this.send(data.offline_message as $mol_offline_web_message) } - override async send(data: $mol_offline_web_message) { - try { - const reg = await this.registration() - reg?.active?.postMessage(data) - } catch (e) { - console.error(e) - } - } - - override run() { - if (! this.registration()) { - this.worker() - return false - } - - // const reg = await this.registration() - // reg?.addEventListener( 'updatefound', ()=> { - // const worker = reg.installing! - // worker.addEventListener( 'statechange', ()=> { - // if( worker.state !== 'activated' ) return - // window.location.reload() - // } ) - // } ) - - return true - } - - _worker = null as null | ServiceWorkerGlobalScope - - 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)) - - return worker - } - - message(event: ExtendableMessageEvent) { + override message(event: ExtendableMessageEvent) { const data = event.data as string | null | $mol_offline_web_message if ( ! data || typeof data !== 'object' ) return @@ -95,17 +32,8 @@ namespace $ { if (data.blacklist) this.blacklist = new Set(data.blacklist) } - before_install(event: Event & { prompt?(): void }) { - event.prompt?.() - } - - install(event: ExtendableEvent) { this.worker().skipWaiting() } - - activate(event: ExtendableEvent) { - // caches.delete( '$mol_offline' ) - - event.waitUntil( this.worker().clients.claim() ) - + override activate(event: ExtendableEvent) { + super.activate(event) $$.$mol_log3_done({ place: '$mol_offline', message: 'Activated', @@ -114,7 +42,7 @@ namespace $ { protected ignore_cache = false - fetch_event(event: FetchEvent) { + override fetch_event(event: FetchEvent) { const request = event.request if( this.blacklist.has( request.url.replace( /^https?:/, '' ) ) ) { diff --git a/worker/worker.ts b/worker/worker.ts new file mode 100644 index 00000000000..2ca7574381e --- /dev/null +++ b/worker/worker.ts @@ -0,0 +1,7 @@ +namespace $ { + export class $mol_worker extends $mol_object { + run() { + return false + } + } +} diff --git a/worker/worker.web.ts b/worker/worker.web.ts new file mode 100644 index 00000000000..3fdfbc28eca --- /dev/null +++ b/worker/worker.web.ts @@ -0,0 +1,126 @@ +/// + +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_object { + 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) {} + + async send(data: unknown) { + try { + const reg = await this.registration() + reg?.active.postMessage(data) + } catch (e) { + console.error(e) + } + } + + 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 + +} From f17f33b15d838df3001d4fd30a1d643edbe11f4a Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 26 Oct 2024 19:21:20 +0300 Subject: [PATCH 12/60] $mol_offline fix refresh --- offline/offline.web.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 727859406f2..9ba86fb56bf 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -18,7 +18,7 @@ namespace $ { } protected window_message(e: MessageEvent) { - const data = typeof e.data === 'object' ? e.data : null + const data = e.data if (data === 'mol_build_obsolete') return this.send({ ignore_cache: true }) if (! data || typeof data !== 'object' || ! ( 'offline_message' in data ) ) return null this.send(data.offline_message as $mol_offline_web_message) From 6efe4549296984228105d0ecb368e7231f0b0447 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 26 Oct 2024 23:49:06 +0300 Subject: [PATCH 13/60] $mol_offline use force-cache --- offline/offline.web.ts | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 9ba86fb56bf..7465c5277df 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -72,7 +72,7 @@ namespace $ { if (this.ignore_cache || request.cache === 'no-cache' || request.cache === 'reload') { // fetch with fallback to cache if statuses not match try { - response = this.fetch_and_cache(request) + response = fetch(request) const actual = await response if (actual.status < 400) return actual @@ -87,16 +87,13 @@ namespace $ { } } - let cached - try { - cached = await caches.match( request ) - } catch (e) { - console.error(e) - } + const forced = new Request(request, { + cache: 'force-cache', + }) - if (! cached) return response ?? this.fetch_and_cache(request) + const cached = await fetch(forced) - if (! fallback_header) return cached + if (! fallback_header || cached.headers.get('$mol_offline_remote_status')) return cached const clone = cached.clone() clone.headers.set( '$mol_offline_remote_status', fallback_header ) @@ -104,21 +101,6 @@ namespace $ { return clone } - cache() { return caches.open( '$mol_offline' ) } - - async fetch_and_cache(request: Request) { - const response = await fetch( request ) - if (response.status !== 200) return response - - try { - await (await this.cache()).put( request , response.clone() ) - } catch (e) { - console.error(e) - } - - return response - } - } $.$mol_offline = $mol_offline_web From 586425581ac96145b13d14518cb7406eddb3b2bb Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 27 Oct 2024 19:37:02 +0300 Subject: [PATCH 14/60] $mol_offline rules refactored, some bugs fixed --- offline/offline.ts | 2 +- offline/offline.web.ts | 46 +++++++++++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/offline/offline.ts b/offline/offline.ts index 853d69ba34e..97e6479e284 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -2,7 +2,7 @@ namespace $ { export type $mol_offline_web_message = { ignore_cache?: boolean - blacklist?: readonly string[] + url_rules?: Record } export class $mol_offline extends $mol_object { diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 7465c5277df..5ea1e12f1cf 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -4,10 +4,6 @@ namespace $ { export class $mol_offline_web extends $mol_worker_web { override path() { return 'web.js' } - protected blacklist = new Set([ - '//cse.google.com/adsense/search/async-ads.js' - ]) - override ready(reg: $mol_worker_reg_active) { reg.active.postMessage({ ignore_cache: false }) } @@ -20,8 +16,12 @@ namespace $ { protected window_message(e: MessageEvent) { const data = e.data if (data === 'mol_build_obsolete') return this.send({ ignore_cache: true }) - if (! data || typeof data !== 'object' || ! ( 'offline_message' in data ) ) return null - this.send(data.offline_message as $mol_offline_web_message) + } + + rules(rules?: NonNullable<$mol_offline_web_message['url_rules']>) { + rules = rules ?? this.url_rules + this.send({ rules }) + return rules } override message(event: ExtendableMessageEvent) { @@ -29,9 +29,10 @@ namespace $ { if ( ! data || typeof data !== 'object' ) return if (data.ignore_cache !== undefined) this.ignore_cache = data.ignore_cache - if (data.blacklist) this.blacklist = new Set(data.blacklist) + if (data.url_rules) this.url_rules = data.url_rules } + override activate(event: ExtendableEvent) { super.activate(event) $$.$mol_log3_done({ @@ -40,12 +41,23 @@ namespace $ { }) } + protected url_rules = { + '//cse.google.com/adsense/search/async-ads.js': 'block', + // '{origin}/mol/app/docs/-/index.html': 'force-cache', + } as NonNullable<$mol_offline_web_message['url_rules']> + protected ignore_cache = false + url_rule(url: string) { + const normalized = url.replace(location.origin, '{origin}').match( /(?:^https?:)?([^&?#]+).*/ )?.[1] ?? '' + return this.url_rules[ normalized ] + } + override fetch_event(event: FetchEvent) { const request = event.request - - if( this.blacklist.has( request.url.replace( /^https?:/, '' ) ) ) { + const url_rule = this.url_rule(request.url) + + if(url_rule === 'block' ) { return event.respondWith( new Response( null, @@ -67,13 +79,15 @@ namespace $ { async respond(request: Request) { let fallback_header - let response - if (this.ignore_cache || request.cache === 'no-cache' || request.cache === 'reload') { + const url_rule = this.url_rule(request.url) + let no_cache = url_rule === 'no-cache' || request.cache === 'no-cache' || request.cache === 'reload' + if (url_rule === 'force-cache') no_cache = false + + if (this.ignore_cache || no_cache) { // fetch with fallback to cache if statuses not match try { - response = fetch(request) - const actual = await response + const actual = await fetch(request) if (actual.status < 400) return actual throw new Error( @@ -88,15 +102,15 @@ namespace $ { } const forced = new Request(request, { - cache: 'force-cache', + cache: 'force-cache' }) const cached = await fetch(forced) if (! fallback_header || cached.headers.get('$mol_offline_remote_status')) return cached - const clone = cached.clone() - clone.headers.set( '$mol_offline_remote_status', fallback_header ) + const clone = new Response(cached.body, cached) + clone.headers.set( '$mol_offline_remote_status', fallback_header ?? '') return clone } From 201dcb21d9a2eca900e6850b91873cf4d065482b Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 27 Oct 2024 21:02:02 +0300 Subject: [PATCH 15/60] $mol_offline configure cached and blocked urls regexp --- offline/install/install.ts | 2 +- offline/offline.ts | 12 ++++---- offline/offline.web.ts | 61 +++++++++++++++++++++++--------------- worker/worker.ts | 2 ++ worker/worker.web.ts | 6 ++-- 5 files changed, 50 insertions(+), 33 deletions(-) diff --git a/offline/install/install.ts b/offline/install/install.ts index dc75de4f4e9..db4f61bb324 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,6 +1,6 @@ namespace $ { try { - new $mol_offline().run() + $mol_offline.main.run() } catch( error ) { console.error( error ) } diff --git a/offline/offline.ts b/offline/offline.ts index 97e6479e284..df70a36fd12 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -2,13 +2,15 @@ namespace $ { export type $mol_offline_web_message = { ignore_cache?: boolean - url_rules?: Record + blocked_urls?: readonly string[] + cached_urls?: readonly string[] } - export class $mol_offline extends $mol_object { - run() { - return false - } + export class $mol_offline extends $mol_worker { + static main = new $mol_offline + + blocked(urls?: readonly string[]) { return urls ?? [] } + cached(urls?: readonly string[]) { return urls ?? [] } } } diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 5ea1e12f1cf..715f423677f 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -2,6 +2,8 @@ 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) { @@ -18,10 +20,16 @@ namespace $ { if (data === 'mol_build_obsolete') return this.send({ ignore_cache: true }) } - rules(rules?: NonNullable<$mol_offline_web_message['url_rules']>) { - rules = rules ?? this.url_rules - this.send({ rules }) - return rules + 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) { @@ -29,7 +37,8 @@ namespace $ { if ( ! data || typeof data !== 'object' ) return if (data.ignore_cache !== undefined) this.ignore_cache = data.ignore_cache - if (data.url_rules) this.url_rules = data.url_rules + 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) } @@ -41,23 +50,27 @@ namespace $ { }) } - protected url_rules = { - '//cse.google.com/adsense/search/async-ads.js': 'block', - // '{origin}/mol/app/docs/-/index.html': 'force-cache', - } as NonNullable<$mol_offline_web_message['url_rules']> + protected blocked_urls = [ + '//cse.google.com/adsense/search/async-ads.js' + ] as readonly string[] - protected ignore_cache = false + 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_rule(url: string) { - const normalized = url.replace(location.origin, '{origin}').match( /(?:^https?:)?([^&?#]+).*/ )?.[1] ?? '' - return this.url_rules[ normalized ] + url_regexp(list: readonly string[]) { + return new RegExp(`#^https?:(?:(?:${list.map(url_reg => url_reg.replace('.', '\.')).join(')|(?:')}))#`) } + protected ignore_cache = false + override fetch_event(event: FetchEvent) { const request = event.request - const url_rule = this.url_rule(request.url) - if(url_rule === 'block' ) { + if( this.blocked_regexp.test(request.url) ) { return event.respondWith( new Response( null, @@ -80,11 +93,15 @@ namespace $ { async respond(request: Request) { let fallback_header - const url_rule = this.url_rule(request.url) - let no_cache = url_rule === 'no-cache' || request.cache === 'no-cache' || request.cache === 'reload' - if (url_rule === 'force-cache') no_cache = false + const force_cache = this.cached_regexp.test(request.url) + const no_cache = request.cache === 'no-cache' && ! force_cache + + if (this.ignore_cache || request.cache === 'reload' || no_cache) { + + if (request.cache !== 'no-cache' && request.cache !== 'reload') { + request = new Request(request, { cache: 'no-cache' }) + } - if (this.ignore_cache || no_cache) { // fetch with fallback to cache if statuses not match try { const actual = await fetch(request) @@ -101,11 +118,7 @@ namespace $ { } } - const forced = new Request(request, { - cache: 'force-cache' - }) - - const cached = await fetch(forced) + const cached = await fetch(new Request(request, { cache: 'force-cache' })) if (! fallback_header || cached.headers.get('$mol_offline_remote_status')) return cached diff --git a/worker/worker.ts b/worker/worker.ts index 2ca7574381e..888d33e481b 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -3,5 +3,7 @@ namespace $ { run() { return false } + + async send(data: unknown) {} } } diff --git a/worker/worker.web.ts b/worker/worker.web.ts index 3fdfbc28eca..c6de834f99e 100644 --- a/worker/worker.web.ts +++ b/worker/worker.web.ts @@ -4,7 +4,7 @@ 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_object { + export class $mol_worker_web extends $mol_worker { path() { return '' } in_worker() { return typeof window === 'undefined' } @@ -70,7 +70,7 @@ namespace $ { ready(reg: $mol_worker_reg_active) {} - async send(data: unknown) { + override async send(data: unknown) { try { const reg = await this.registration() reg?.active.postMessage(data) @@ -79,7 +79,7 @@ namespace $ { } } - run() { + override run() { if (this.registration()) return true this.worker() return false From cd84bfab112c3eff7ba74009a2aaaf725dd3e821 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 27 Oct 2024 21:30:09 +0300 Subject: [PATCH 16/60] $mol_offline fix regexp --- offline/offline.web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 715f423677f..1cc44e5d5e1 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -51,18 +51,18 @@ namespace $ { } protected blocked_urls = [ - '//cse.google.com/adsense/search/async-ads.js' + '//cse\.google\.com/adsense/search/async-ads\.js' ] as readonly string[] protected cached_urls = [ - '.*/index.html' + '.*/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.map(url_reg => url_reg.replace('.', '\.')).join(')|(?:')}))#`) + return new RegExp(`#^https?:(?:(?:${list.join(')|(?:')}))#`) } protected ignore_cache = false From 958e207eede87f34b21ad134b082b90597a3554e Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 00:50:10 +0300 Subject: [PATCH 17/60] $mol_worker_service plugins, refactor --- offline/install/install.ts | 6 +- offline/offline.ts | 25 +++--- offline/offline.web.ts | 80 ++--------------- worker/service/plugin.ts | 49 +++++++++++ worker/service/worker.ts | 26 ++++++ worker/service/worker.web.ts | 164 +++++++++++++++++++++++++++++++++++ worker/worker.ts | 9 -- worker/worker.web.ts | 126 --------------------------- 8 files changed, 263 insertions(+), 222 deletions(-) create mode 100644 worker/service/plugin.ts create mode 100644 worker/service/worker.ts create mode 100644 worker/service/worker.web.ts delete mode 100644 worker/worker.ts delete mode 100644 worker/worker.web.ts diff --git a/offline/install/install.ts b/offline/install/install.ts index db4f61bb324..99b60b98b5c 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 df70a36fd12..309500d1741 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 1cc44e5d5e1..37f62b7cf84 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 00000000000..162bd58cbcc --- /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 00000000000..f54dd424f7c --- /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 00000000000..bce4033419e --- /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 888d33e481b..00000000000 --- 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 c6de834f99e..00000000000 --- 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 - -} From 2dcd5aa7df5d5e4b16372e62b8e696a17a75883e Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 01:19:09 +0300 Subject: [PATCH 18/60] $mol_offline save ignore_cache between window.reload() --- offline/offline.ts | 12 ++++++++++-- offline/offline.web.ts | 8 ++++++-- worker/service/worker.web.ts | 1 - 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/offline/offline.ts b/offline/offline.ts index 309500d1741..4a28e90db1b 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -4,13 +4,21 @@ namespace $ { constructor() { super() this.$.$mol_dom_context.addEventListener('message', e => { - if (e.data === 'mol_build_obsolete') this.value('ignore_cache', true) + if (e.data === 'mol_build_obsolete') this.ignore_cache(this.value('ignore_cache', true)) }) } + protected ignore_cache(next?: null | boolean) { + return this.$.$mol_state_session.value(`${this}.ignore_cache()`, next) + } + + @ $mol_mem override defaults() { + const ignore_cache = this.ignore_cache() + this.ignore_cache(null) + return { - ignore_cache: false, + ignore_cache: 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 37f62b7cf84..0dcb6b4793d 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -4,7 +4,10 @@ namespace $ { override fetch_event(event: FetchEvent) { const request = event.request - if( this.value('blocked_urls')?.includes(request.url.replace( /^https?:/, '' )) ) { + 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, @@ -31,8 +34,9 @@ namespace $ { const force_cache = /.+\/[^\/]+\.html/.test(request.url) const no_cache = request.cache === 'no-cache' && ! force_cache + const ignore_cache = this.value('ignore_cache') - if (this.value('ignore_cache') || request.cache === 'reload' || no_cache) { + if (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/worker.web.ts b/worker/service/worker.web.ts index bce4033419e..9f026341a8b 100644 --- a/worker/service/worker.web.ts +++ b/worker/service/worker.web.ts @@ -48,7 +48,6 @@ namespace $ { return reg } - static installing(reg: $mol_worker_service_reg_installing) { reg.addEventListener( 'updatefound', this.update_found.bind(this, reg.installing)) } From 08aa70c8b5dbffe081b9f5adc459996f966bfbef Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 01:29:20 +0300 Subject: [PATCH 19/60] $mol_offline fix test.html caching --- offline/offline.web.ts | 2 +- worker/service/plugin.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 0dcb6b4793d..35d55b873aa 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -32,7 +32,7 @@ namespace $ { async respond(request: Request) { let fallback_header - const force_cache = /.+\/[^\/]+\.html/.test(request.url) + const force_cache = /.+\/index\.html/.test(request.url) const no_cache = request.cache === 'no-cache' && ! force_cache const ignore_cache = this.value('ignore_cache') diff --git a/worker/service/plugin.ts b/worker/service/plugin.ts index 162bd58cbcc..735e58d88b4 100644 --- a/worker/service/plugin.ts +++ b/worker/service/plugin.ts @@ -3,7 +3,7 @@ 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, + worker: { attach(plugin: $mol_worker_service_plugin): void }, config?: Partial< InstanceType< This > >, ) { const plugin = new this From 3593a0ab99643dcd517d208eaeeb759c55222ab6 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 12:55:48 +0300 Subject: [PATCH 20/60] $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 99b60b98b5c..e16848bd33f 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 4a28e90db1b..d0f264df320 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 35d55b873aa..00000000000 --- 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 735e58d88b4..00000000000 --- 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 f54dd424f7c..d0d56a90da2 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 9f026341a8b..e6ab988b2c2 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 From e25b6776caf9676bb07ae7924706fa5fcaff9045 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 13:01:11 +0300 Subject: [PATCH 21/60] $mol_worker fix worker singletone --- worker/service/worker.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/worker/service/worker.ts b/worker/service/worker.ts index d0d56a90da2..7929293866d 100644 --- a/worker/service/worker.ts +++ b/worker/service/worker.ts @@ -5,9 +5,9 @@ namespace $ { static path() { return 'web.js' } - static plugins = {} as Record + protected static plugins = {} as Record - static inited = false + protected static inited = false static init() {} static ready() {} @@ -19,12 +19,16 @@ namespace $ { this : This, config?: Partial< InstanceType< This > >, ) { - if ( ! this.inited ) { - this.init() - this.inited = true - } const plugin = this.make(config ?? {}) - this.plugins[plugin.id] = plugin + + const worker = this.$.$mol_worker_service + + worker.plugins[plugin.id] = plugin + + if ( ! worker.inited ) { + worker.init() + worker.inited = true + } return plugin } From db14037d7cf3ed1917412d8116e6e0c23edade0a Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 13:07:07 +0300 Subject: [PATCH 22/60] $mol_worker fix try/catch in attach --- worker/service/worker.ts | 18 +++++++++++------- worker/service/worker.web.ts | 4 ++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/worker/service/worker.ts b/worker/service/worker.ts index 7929293866d..e653105e0e8 100644 --- a/worker/service/worker.ts +++ b/worker/service/worker.ts @@ -21,13 +21,17 @@ namespace $ { ) { const plugin = this.make(config ?? {}) - const worker = this.$.$mol_worker_service - - worker.plugins[plugin.id] = plugin - - if ( ! worker.inited ) { - worker.init() - worker.inited = true + try { + const worker = this.$.$mol_worker_service + + worker.plugins[plugin.id] = plugin + + if ( ! worker.inited ) { + worker.init() + worker.inited = true + } + } catch (error) { + console.error(error) } return plugin diff --git a/worker/service/worker.web.ts b/worker/service/worker.web.ts index e6ab988b2c2..d55174d2664 100644 --- a/worker/service/worker.web.ts +++ b/worker/service/worker.web.ts @@ -123,13 +123,13 @@ namespace $ { const request = event.request for (let name in this.plugins) { - if (this.plugins[name]?.blocked(request)) { + 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) + const response = this.plugins[name].modify(request) if (response) return event.respondWith(response) } } From bb46b573098caf72abe62dc005f101d2073bb679 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 21:06:09 +0300 Subject: [PATCH 23/60] $mol_worker_service renamed to $mol_service, $mol_offline refactor to $mol_service --- notify/notify.meta.tree | 1 - notify/notify.node.ts | 20 ----- notify/notify.ts | 24 ++++++ notify/notify.web.ts | 64 ++------------- notify/service/service.ts | 14 ++++ notify/service/service.web.ts | 40 +++++++++ offline/offline.ts | 2 +- service/service.ts | 68 +++++++++++---- .../worker.web.ts => service/service.web.ts | 82 +++++++++++++++---- worker/service/worker.ts | 60 -------------- 10 files changed, 204 insertions(+), 171 deletions(-) delete mode 100644 notify/notify.meta.tree delete mode 100644 notify/notify.node.ts create mode 100644 notify/notify.ts create mode 100644 notify/service/service.ts create mode 100644 notify/service/service.web.ts rename worker/service/worker.web.ts => service/service.web.ts (55%) delete mode 100644 worker/service/worker.ts diff --git a/notify/notify.meta.tree b/notify/notify.meta.tree deleted file mode 100644 index 49489eb16b2..00000000000 --- a/notify/notify.meta.tree +++ /dev/null @@ -1 +0,0 @@ -include \/mol/offline/install diff --git a/notify/notify.node.ts b/notify/notify.node.ts deleted file mode 100644 index 71d3fb651ae..00000000000 --- a/notify/notify.node.ts +++ /dev/null @@ -1,20 +0,0 @@ -namespace $ { - - export class $mol_notify { - - @ $mol_mem - static allowed( next?: boolean ) { - return false - } - - @ $mol_action - static show( info: { - context: string, - message: string, - uri: string - } ) { - } - - } - -} diff --git a/notify/notify.ts b/notify/notify.ts new file mode 100644 index 00000000000..a983d5363d5 --- /dev/null +++ b/notify/notify.ts @@ -0,0 +1,24 @@ +namespace $ { + + export type $mol_notify_info = { + context: string, + message: string, + uri: string + } + + export class $mol_notify extends $mol_object { + + @ $mol_mem + static allowed( next?: boolean ) { + return false + } + + static show( info: $mol_notify_info ) { + this.$.$mol_service.send(info) + } + + } + + $mol_notify_service.attach() + +} diff --git a/notify/notify.web.ts b/notify/notify.web.ts index c3c16435671..872d734331f 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -1,12 +1,13 @@ namespace $ { /** Manages system notifications. Notifications of same context are auto joined to one notification. */ - export class $mol_notify { + export class $mol_notify_web extends $mol_notify { @ $mol_mem - static allowed( next?: boolean ) { + static override allowed( next?: boolean ) { - let perm = Notification.permission + + let perm = this.$.$mol_dom_context.Notification.permission if( next === undefined ) return perm === 'granted' if( perm === 'granted' ) return true @@ -16,67 +17,16 @@ namespace $ { return perm === 'granted' } - static async request_permissions() { + static request_permissions() { return new Promise< NotificationPermission >( done => - Notification.requestPermission( perm => { + this.$.$mol_dom_context.Notification.requestPermission( perm => { done( perm ) } ) ) } - @ $mol_action - static show( info: { - context: string, - message: string, - uri: string - } ) { - navigator.serviceWorker.controller!.postMessage( info ) - } - } - - if( typeof window === 'undefined' ) { - - self.addEventListener( 'message', async event => { - - let { context: title, message: body, uri: data } = event.data - const tag = data - - const existen = await $mol_service().getNotifications({ tag }) - - for( const not of existen ) { - - if( not.body.indexOf( body ) !== -1 ) body = not.body - else if( body.indexOf( not.body ) === -1 ) body = not.body + '\n' + body - - not.close() - } - - // const vibrate = [ 100, 200, 300, 400, 500 ] - - await $mol_service().showNotification( title, { body, data, /*vibrate,*/ tag } ) - - } ) - - self.addEventListener( 'notificationclick', $mol_service_handler( async ( event: any )=> { - - const clients: any[] = await ( self as any ).clients.matchAll({ includeUncontrolled: true }) - event.notification.close() - if( clients.length ) { - - const last = clients[ clients.length - 1 ] - await last.focus() - await last.navigate( event.notification.data ) - - } else { - - await ( self as any ).clients.openWindow( event.notification.data ) - - } - - } ) ) - - } + $.$mol_notify = $mol_notify_web } diff --git a/notify/service/service.ts b/notify/service/service.ts new file mode 100644 index 00000000000..555c857ed58 --- /dev/null +++ b/notify/service/service.ts @@ -0,0 +1,14 @@ +namespace $ { + export class $mol_notify_service extends $mol_service { + override message_data(data: {}) { + if ('uri' in data && 'message' in data) { + this.info(data as $mol_notify_info) + } + return null + } + + async info(info: $mol_notify_info) {} + + } + +} diff --git a/notify/service/service.web.ts b/notify/service/service.web.ts new file mode 100644 index 00000000000..f0057829192 --- /dev/null +++ b/notify/service/service.web.ts @@ -0,0 +1,40 @@ +namespace $ { + export class $mol_notify_service_web extends $mol_notify_service { + override async info({ context: title, message: body, uri: data }: $mol_notify_info) { + const worker = this.$.$mol_service_web.worker() + const tag = data + const existen = await worker.registration.getNotifications({ tag }) + + for( const not of existen ) { + + if( not.body.indexOf( body ) !== -1 ) body = not.body + else if( body.indexOf( not.body ) === -1 ) body = not.body + '\n' + body + + not.close() + } + + // const vibrate = [ 100, 200, 300, 400, 500 ] + + await worker.registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) + + } + + override async notification_click( notification: Notification ) { + const worker = this.$.$mol_service_web.worker() + + const clients = await worker.clients.matchAll({ includeUncontrolled: true, type: 'window' }) + const last = clients.at(-1) + + if( last ) { + await last.focus() + await last.navigate( notification.data ) + + return + } + + await worker.clients.openWindow( notification.data ) + } + } + + $.$mol_notify_service = $mol_notify_service_web +} diff --git a/offline/offline.ts b/offline/offline.ts index d0f264df320..8645bc966e0 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,6 +1,6 @@ namespace $ { - export class $mol_offline extends $mol_worker_service { + export class $mol_offline extends $mol_service { blocked_urls = [ '//cse.google.com/adsense/search/async-ads.js' ] diff --git a/service/service.ts b/service/service.ts index ed4a8bcea6e..bd85f12e397 100644 --- a/service/service.ts +++ b/service/service.ts @@ -1,21 +1,61 @@ namespace $ { + + export class $mol_service extends $mol_object { + static in_worker() { return typeof window === 'undefined' } + + static path() { return 'web.js' } + + protected static plugins = {} as Record + + protected static inited = false + static init() {} + static ready() {} + + @ $mol_action + static send(data: {}) {} + + static attach< This extends typeof $mol_service >( + this : This, + config?: Partial< InstanceType< This > >, + ) { + const plugin = this.make(config ?? {}) + + try { + const worker = this.$.$mol_service - function worker() { - return navigator.serviceWorker.ready - } + worker.plugins[plugin.id] = plugin - export function $mol_service() { - if( typeof window === 'undefined' ) { - return ( self as any ).registration as ServiceWorkerRegistration - } else { - return $mol_wire_sync( worker )() + if ( ! worker.inited ) { + worker.init() + worker.inited = true + } + } catch (error) { + console.error(error) + } + + return plugin } - } - - export function $mol_service_handler< E extends Event >( handle : ( event: E )=> Promise ) { - return ( event: E )=> { - ;( event as any ).waitUntil( handle( event ) ) + + 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() {} + message_data(data: {}) { return null as null | undefined | Promise } + notification_click(notification: Notification) { return null as null | undefined | Promise } } - } diff --git a/worker/service/worker.web.ts b/service/service.web.ts similarity index 55% rename from worker/service/worker.web.ts rename to service/service.web.ts index d55174d2664..e59cd850ed5 100644 --- a/worker/service/worker.web.ts +++ b/service/service.web.ts @@ -1,10 +1,10 @@ /// namespace $ { - export type $mol_worker_service_reg_active = ServiceWorkerRegistration & { active: ServiceWorker } - export type $mol_worker_service_reg_installing = ServiceWorkerRegistration & { installing: ServiceWorker } + export type $mol_service_reg_active = ServiceWorkerRegistration & { active: ServiceWorker } + export type $mol_service_reg_installing = ServiceWorkerRegistration & { installing: ServiceWorker } - export class $mol_worker_service_web extends $mol_worker_service { + export class $mol_service_web extends $mol_service { static is_supported() { if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) { console.warn( 'HTTPS or localhost is required for service workers.' ) @@ -19,7 +19,13 @@ namespace $ { return true } - protected static registration = null as null | $mol_worker_service_reg_active + static handler< E extends ExtendableEvent >( handle : ( event: E )=> Promise ) { + return ( event: E )=> { + event.waitUntil( handle( event ) ) + } + } + + protected static registration = null as null | $mol_service_reg_active static override init() { if ( this.in_worker() ) { @@ -31,19 +37,26 @@ namespace $ { this.registration_init() } + static async registration_init() { try { const reg_promise = navigator.serviceWorker.register(this.path()) - 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) + const ready = navigator.serviceWorker.ready as Promise<$mol_service_reg_active> + const reg = (await reg_promise) as $mol_service_reg_active + if (reg.installing) this.installing(reg as $mol_service_reg_installing) await ready this.registration = reg this.ready() + for (const data of this.send_delayed) { + this.send(data) + } + + this.send_delayed = [] + return true } catch (error) { console.error(error) @@ -51,7 +64,7 @@ namespace $ { } } - static installing(reg: $mol_worker_service_reg_installing) { + static installing(reg: $mol_service_reg_installing) { reg.addEventListener( 'updatefound', this.update_found.bind(this, reg.installing)) } @@ -67,28 +80,61 @@ namespace $ { static worker() { 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)) - worker.addEventListener( 'activate' , this.activate.bind(this)) - worker.addEventListener( 'message', this.message.bind(this)) - worker.addEventListener( 'fetch', this.fetch_event.bind(this)) + if (! this.inited) { + 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)) + worker.addEventListener( 'notificationclick', this.notification_click.bind(this)) + } // for (let name in this.plugins) this.plugins[name].init(worker) return worker } + static notification_click(event: NotificationEvent) { + let promises = [] + + for (let name in this.plugins) { + const result = this.plugins[name].notification_click(event.notification) + if (result) promises.push(result) + } + + if (promises.length > 0) { + event.waitUntil(Promise.all(promises)) + } + } + + protected static send_delayed = [] as {}[] + + @ $mol_action + static override send(data: {}) { + const active = this.registration?.active + + if (active) { + active.postMessage(data) + } else { + this.send_delayed.push(data) + } + } + static message(event: ExtendableMessageEvent) { const data = event.data as string | null | { [k: string]: unknown } if ( ! data || typeof data !== 'object' ) return false - this.data(data) + let promises = [] + for (let name in this.plugins) { + const result = this.plugins[name].message_data(data) + if (result) promises.push(result) + } - return true + if (promises.length > 0) { + event.waitUntil(Promise.all(promises)) + } } static before_install(event: Event & { prompt?(): void }) { @@ -136,6 +182,6 @@ namespace $ { } - $.$mol_worker_service = $mol_worker_service_web + $.$mol_service = $mol_service_web } diff --git a/worker/service/worker.ts b/worker/service/worker.ts deleted file mode 100644 index e653105e0e8..00000000000 --- a/worker/service/worker.ts +++ /dev/null @@ -1,60 +0,0 @@ -namespace $ { - - export class $mol_worker_service extends $mol_object { - static in_worker() { return typeof window === 'undefined' } - - static path() { return 'web.js' } - - protected static plugins = {} as Record - - protected static inited = false - static init() {} - static ready() {} - - static data(next?: unknown) { - return next ?? null - } - - static attach< This extends typeof $mol_worker_service >( - this : This, - config?: Partial< InstanceType< This > >, - ) { - const plugin = this.make(config ?? {}) - - try { - const worker = this.$.$mol_worker_service - - worker.plugins[plugin.id] = plugin - - if ( ! worker.inited ) { - worker.init() - worker.inited = true - } - } catch (error) { - console.error(error) - } - - return plugin - } - - 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() {} - } -} From 121c90e7585994140c25ed9efef88c8dba60afb2 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 21:24:01 +0300 Subject: [PATCH 24/60] $mol_service fix init hook --- service/service.ts | 14 ++++++++------ service/service.web.ts | 19 +++++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/service/service.ts b/service/service.ts index bd85f12e397..62b544ffb3d 100644 --- a/service/service.ts +++ b/service/service.ts @@ -8,8 +8,7 @@ namespace $ { protected static plugins = {} as Record protected static inited = false - static init() {} - static ready() {} + protected static init() {} @ $mol_action static send(data: {}) {} @@ -48,14 +47,17 @@ namespace $ { ) } - blocked(res: Request) { return false } - modify(res: Request) { return null as null | Response | PromiseLike } + init() {} before_install() {} - install() {} - activate() {} + install() { return null as undefined | null | Promise } + activate() { return null as undefined | null | Promise } state_change() {} message_data(data: {}) { return null as null | undefined | Promise } + + blocked(res: Request) { return false } + modify(res: Request) { return null as null | Response | PromiseLike } + notification_click(notification: Notification) { return null as null | undefined | Promise } } } diff --git a/service/service.web.ts b/service/service.web.ts index e59cd850ed5..71fc91f227b 100644 --- a/service/service.web.ts +++ b/service/service.web.ts @@ -49,7 +49,6 @@ namespace $ { await ready this.registration = reg - this.ready() for (const data of this.send_delayed) { this.send(data) @@ -87,10 +86,10 @@ namespace $ { worker.addEventListener( 'message', this.message.bind(this)) worker.addEventListener( 'fetch', this.fetch_event.bind(this)) worker.addEventListener( 'notificationclick', this.notification_click.bind(this)) + this.inited = true + for (let name in this.plugins) this.plugins[name].init() } - // for (let name in this.plugins) this.plugins[name].init(worker) - return worker } @@ -146,18 +145,26 @@ namespace $ { } static install(event: ExtendableEvent) { + const promises = [] for (let name in this.plugins) { - this.plugins[name].install() + const result = this.plugins[name].install() + if (result) promises.push(result) + } + + if (promises.length > 0) { + event.waitUntil(Promise.all(promises)) } this.worker().skipWaiting() } static activate(event: ExtendableEvent) { + const promises = [] for (let name in this.plugins) { - this.plugins[name].activate() + const result = this.plugins[name].activate() + if (result) promises.push(result) } - event.waitUntil( this.worker().clients.claim() ) + event.waitUntil( Promise.all([ ...promises, this.worker().clients.claim() ]) ) this.$.$mol_log3_done({ place: `${this}.activate()`, From 9f86a713b82df54a3f03381e53131de93486b1ee Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 22:38:26 +0300 Subject: [PATCH 25/60] $mol_service refactor extracted fetch service and notify --- fetch/service/service.ts | 24 +++++++++++ fetch/service/service.web.ts | 27 +++++++++++++ notify/service/service.web.ts | 11 ++++- offline/offline.ts | 2 +- service/service.ts | 46 +++++++-------------- service/service.web.ts | 75 +++++++++-------------------------- 6 files changed, 94 insertions(+), 91 deletions(-) create mode 100644 fetch/service/service.ts create mode 100644 fetch/service/service.web.ts diff --git a/fetch/service/service.ts b/fetch/service/service.ts new file mode 100644 index 00000000000..d55f3703398 --- /dev/null +++ b/fetch/service/service.ts @@ -0,0 +1,24 @@ +namespace $ { + export class $mol_fetch_service extends $mol_service { + protected static plugins = new Set<$mol_fetch_service>() + + protected static override add(plugin: $mol_fetch_service) { + if (! this.inited) this.$.$mol_service.add(new this) + this.inited = true + this.$.$mol_fetch_service.plugins.add(plugin) + } + + 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 } + } +} diff --git a/fetch/service/service.web.ts b/fetch/service/service.web.ts new file mode 100644 index 00000000000..8424d19e18f --- /dev/null +++ b/fetch/service/service.web.ts @@ -0,0 +1,27 @@ +namespace $ { + export class $mol_fetch_service_web extends $mol_fetch_service { + override init() { + const service = this.$.$mol_service_web + const worker = service.worker() + worker.addEventListener('fetch', this.$.$mol_fetch_service_web.fetch_event.bind(this)) + } + + static fetch_event(event: FetchEvent) { + const request = event.request + const plugins = this.$.$mol_fetch_service.plugins + + for (const plugin of plugins) { + if (plugin.blocked(request)) { + return event.respondWith(this.blocked_response()) + } + } + + for (const plugin of plugins) { + const response = plugin.modify(request) + if (response) return event.respondWith(response) + } + } + } + + $.$mol_fetch_service = $mol_fetch_service_web +} diff --git a/notify/service/service.web.ts b/notify/service/service.web.ts index f0057829192..a1a90b8ec50 100644 --- a/notify/service/service.web.ts +++ b/notify/service/service.web.ts @@ -1,5 +1,14 @@ namespace $ { export class $mol_notify_service_web extends $mol_notify_service { + override init() { + const worker = this.$.$mol_service_web.worker() + worker.addEventListener( 'notificationclick', this.notification_click_event.bind(this)) + } + + protected notification_click_event(event: NotificationEvent) { + event.waitUntil(this.notification_click(event.notification)) + } + override async info({ context: title, message: body, uri: data }: $mol_notify_info) { const worker = this.$.$mol_service_web.worker() const tag = data @@ -19,7 +28,7 @@ namespace $ { } - override async notification_click( notification: Notification ) { + async notification_click( notification: Notification ) { const worker = this.$.$mol_service_web.worker() const clients = await worker.clients.matchAll({ includeUncontrolled: true, type: 'window' }) diff --git a/offline/offline.ts b/offline/offline.ts index 8645bc966e0..2ae3a221747 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,6 +1,6 @@ namespace $ { - export class $mol_offline extends $mol_service { + export class $mol_offline extends $mol_fetch_service { blocked_urls = [ '//cse.google.com/adsense/search/async-ads.js' ] diff --git a/service/service.ts b/service/service.ts index 62b544ffb3d..5ebb5606ccc 100644 --- a/service/service.ts +++ b/service/service.ts @@ -5,59 +5,41 @@ namespace $ { static path() { return 'web.js' } - protected static plugins = {} as Record - protected static inited = false protected static init() {} @ $mol_action static send(data: {}) {} + protected static plugins = new Set<$mol_service>() + + protected static add(plugin: $mol_service) { + const service = this.$.$mol_service + + if ( ! service.inited ) { + service.init() + service.inited = true + } + + service.plugins.add(plugin) + } + static attach< This extends typeof $mol_service >( this : This, config?: Partial< InstanceType< This > >, ) { const plugin = this.make(config ?? {}) - try { - const worker = this.$.$mol_service - - worker.plugins[plugin.id] = plugin - - if ( ! worker.inited ) { - worker.init() - worker.inited = true - } - } catch (error) { - console.error(error) - } + this.add(plugin) return plugin } - id = this.toString() - - static blocked_response() { - return new Response( - null, - { - status: 418, - statusText: 'Blocked' - }, - ) - } - - init() {} before_install() {} install() { return null as undefined | null | Promise } activate() { return null as undefined | null | Promise } state_change() {} message_data(data: {}) { return null as null | undefined | Promise } - - blocked(res: Request) { return false } - modify(res: Request) { return null as null | Response | PromiseLike } - - notification_click(notification: Notification) { return null as null | undefined | Promise } } } diff --git a/service/service.web.ts b/service/service.web.ts index 71fc91f227b..8a99d92e9ef 100644 --- a/service/service.web.ts +++ b/service/service.web.ts @@ -19,9 +19,10 @@ namespace $ { return true } - static handler< E extends ExtendableEvent >( handle : ( event: E )=> Promise ) { + static handler< E extends ExtendableEvent >( handle : ( event: E )=> null | undefined | Promise ) { return ( event: E )=> { - event.waitUntil( handle( event ) ) + const result = handle( event ) + if (result) event.waitUntil( result ) } } @@ -72,8 +73,8 @@ namespace $ { } static state_change(worker: ServiceWorker) { - for (let name in this.plugins) { - this.plugins[name].state_change() + for (const plugin of this.plugins) { + plugin.state_change() } } @@ -84,28 +85,13 @@ namespace $ { 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)) - worker.addEventListener( 'notificationclick', this.notification_click.bind(this)) this.inited = true - for (let name in this.plugins) this.plugins[name].init() + for (const plugin of this.plugins) plugin.init() } return worker } - static notification_click(event: NotificationEvent) { - let promises = [] - - for (let name in this.plugins) { - const result = this.plugins[name].notification_click(event.notification) - if (result) promises.push(result) - } - - if (promises.length > 0) { - event.waitUntil(Promise.all(promises)) - } - } - protected static send_delayed = [] as {}[] @ $mol_action @@ -125,46 +111,36 @@ namespace $ { } if ( ! data || typeof data !== 'object' ) return false - let promises = [] - for (let name in this.plugins) { - const result = this.plugins[name].message_data(data) - if (result) promises.push(result) - } - - if (promises.length > 0) { - event.waitUntil(Promise.all(promises)) + for (const plugin of this.plugins) { + const result = plugin.message_data(data) + if (result) event.waitUntil(result) } } static before_install(event: Event & { prompt?(): void }) { - for (let name in this.plugins) { - this.plugins[name].before_install() + for (const plugin of this.plugins) { + plugin.before_install() } event.prompt?.() } static install(event: ExtendableEvent) { - const promises = [] - for (let name in this.plugins) { - const result = this.plugins[name].install() - if (result) promises.push(result) + for (const plugin of this.plugins) { + const result = plugin.install() + if (result) event.waitUntil(result) } - if (promises.length > 0) { - event.waitUntil(Promise.all(promises)) - } this.worker().skipWaiting() } static activate(event: ExtendableEvent) { - const promises = [] - for (let name in this.plugins) { - const result = this.plugins[name].activate() - if (result) promises.push(result) + for (const plugin of this.plugins) { + const result = plugin.activate() + if (result) event.waitUntil(result) } - event.waitUntil( Promise.all([ ...promises, this.worker().clients.claim() ]) ) + event.waitUntil( this.worker().clients.claim() ) this.$.$mol_log3_done({ place: `${this}.activate()`, @@ -172,21 +148,6 @@ namespace $ { }) } - static fetch_event(event: FetchEvent) { - const request = event.request - - for (let name in this.plugins) { - 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) - } - } - } $.$mol_service = $mol_service_web From 7d3ce8a24c83d018500f8e7e3d03ed72327410aa Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 23:00:50 +0300 Subject: [PATCH 26/60] $mol_fetch_service fix bug with fetch_event binded scope --- fetch/service/service.web.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fetch/service/service.web.ts b/fetch/service/service.web.ts index 8424d19e18f..9945466af44 100644 --- a/fetch/service/service.web.ts +++ b/fetch/service/service.web.ts @@ -1,22 +1,22 @@ namespace $ { export class $mol_fetch_service_web extends $mol_fetch_service { override init() { - const service = this.$.$mol_service_web - const worker = service.worker() - worker.addEventListener('fetch', this.$.$mol_fetch_service_web.fetch_event.bind(this)) + const worker = this.$.$mol_service_web.worker() + const fetch_service = this.$.$mol_fetch_service_web + + worker.addEventListener('fetch', fetch_service.fetch_event.bind(fetch_service)) } static fetch_event(event: FetchEvent) { const request = event.request - const plugins = this.$.$mol_fetch_service.plugins - for (const plugin of plugins) { + for (const plugin of this.plugins) { if (plugin.blocked(request)) { return event.respondWith(this.blocked_response()) } } - for (const plugin of plugins) { + for (const plugin of this.plugins) { const response = plugin.modify(request) if (response) return event.respondWith(response) } From b168b72f1e67b5049e36058699f3a1990579e602 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 29 Oct 2024 23:15:32 +0300 Subject: [PATCH 27/60] $mol_fetch_service refactor simplify add logic --- fetch/service/service.ts | 4 +++- service/service.ts | 12 +----------- service/service.web.ts | 9 ++++++++- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/fetch/service/service.ts b/fetch/service/service.ts index d55f3703398..fa201761d5e 100644 --- a/fetch/service/service.ts +++ b/fetch/service/service.ts @@ -2,10 +2,12 @@ namespace $ { export class $mol_fetch_service extends $mol_service { protected static plugins = new Set<$mol_fetch_service>() + protected static inited = false + protected static override add(plugin: $mol_fetch_service) { + this.plugins.add(plugin) if (! this.inited) this.$.$mol_service.add(new this) this.inited = true - this.$.$mol_fetch_service.plugins.add(plugin) } static blocked_response() { diff --git a/service/service.ts b/service/service.ts index 5ebb5606ccc..bbf64114eb9 100644 --- a/service/service.ts +++ b/service/service.ts @@ -5,23 +5,13 @@ namespace $ { static path() { return 'web.js' } - protected static inited = false - protected static init() {} - @ $mol_action static send(data: {}) {} protected static plugins = new Set<$mol_service>() protected static add(plugin: $mol_service) { - const service = this.$.$mol_service - - if ( ! service.inited ) { - service.init() - service.inited = true - } - - service.plugins.add(plugin) + this.plugins.add(plugin) } static attach< This extends typeof $mol_service >( diff --git a/service/service.web.ts b/service/service.web.ts index 8a99d92e9ef..dd93fbe7d17 100644 --- a/service/service.web.ts +++ b/service/service.web.ts @@ -28,7 +28,13 @@ namespace $ { protected static registration = null as null | $mol_service_reg_active - static override init() { + protected static inited = false + + protected static override add(plugin: $mol_service) { + super.add(plugin) + + if (this.inited) return + if ( this.in_worker() ) { this.worker() return @@ -36,6 +42,7 @@ namespace $ { if ( ! this.is_supported() ) return this.registration_init() + this.inited = true } From 354468b9792a9ae9ff20313636c913b54270d05d Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Wed, 30 Oct 2024 10:06:52 +0300 Subject: [PATCH 28/60] $mol_service_plugin extracted, used static, fetch hook moved back to $mol_service --- fetch/service/service.ts | 26 -------------------------- fetch/service/service.web.ts | 27 --------------------------- notify/notify.ts | 2 +- notify/service/service.ts | 6 +++--- notify/service/service.web.ts | 8 ++++---- offline/install/install.ts | 2 +- offline/offline.ts | 10 +++++----- service/plugin.ts | 13 +++++++++++++ service/service.ts | 29 ++++++++++++----------------- service/service.web.ts | 20 ++++++++++++++++---- 10 files changed, 55 insertions(+), 88 deletions(-) delete mode 100644 fetch/service/service.ts delete mode 100644 fetch/service/service.web.ts create mode 100644 service/plugin.ts diff --git a/fetch/service/service.ts b/fetch/service/service.ts deleted file mode 100644 index fa201761d5e..00000000000 --- a/fetch/service/service.ts +++ /dev/null @@ -1,26 +0,0 @@ -namespace $ { - export class $mol_fetch_service extends $mol_service { - protected static plugins = new Set<$mol_fetch_service>() - - protected static inited = false - - protected static override add(plugin: $mol_fetch_service) { - this.plugins.add(plugin) - if (! this.inited) this.$.$mol_service.add(new this) - this.inited = true - } - - 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 } - } -} diff --git a/fetch/service/service.web.ts b/fetch/service/service.web.ts deleted file mode 100644 index 9945466af44..00000000000 --- a/fetch/service/service.web.ts +++ /dev/null @@ -1,27 +0,0 @@ -namespace $ { - export class $mol_fetch_service_web extends $mol_fetch_service { - override init() { - const worker = this.$.$mol_service_web.worker() - const fetch_service = this.$.$mol_fetch_service_web - - worker.addEventListener('fetch', fetch_service.fetch_event.bind(fetch_service)) - } - - static fetch_event(event: FetchEvent) { - const request = event.request - - for (const plugin of this.plugins) { - if (plugin.blocked(request)) { - return event.respondWith(this.blocked_response()) - } - } - - for (const plugin of this.plugins) { - const response = plugin.modify(request) - if (response) return event.respondWith(response) - } - } - } - - $.$mol_fetch_service = $mol_fetch_service_web -} diff --git a/notify/notify.ts b/notify/notify.ts index a983d5363d5..c1e13a7897b 100644 --- a/notify/notify.ts +++ b/notify/notify.ts @@ -19,6 +19,6 @@ namespace $ { } - $mol_notify_service.attach() + $mol_service.attach($mol_notify_service) } diff --git a/notify/service/service.ts b/notify/service/service.ts index 555c857ed58..215e984042c 100644 --- a/notify/service/service.ts +++ b/notify/service/service.ts @@ -1,13 +1,13 @@ namespace $ { - export class $mol_notify_service extends $mol_service { - override message_data(data: {}) { + export class $mol_notify_service extends $mol_service_plugin { + static override message_data(data: {}) { if ('uri' in data && 'message' in data) { this.info(data as $mol_notify_info) } return null } - async info(info: $mol_notify_info) {} + static async info(info: $mol_notify_info) {} } diff --git a/notify/service/service.web.ts b/notify/service/service.web.ts index a1a90b8ec50..12adb79610b 100644 --- a/notify/service/service.web.ts +++ b/notify/service/service.web.ts @@ -1,15 +1,15 @@ namespace $ { export class $mol_notify_service_web extends $mol_notify_service { - override init() { + static override init() { const worker = this.$.$mol_service_web.worker() worker.addEventListener( 'notificationclick', this.notification_click_event.bind(this)) } - protected notification_click_event(event: NotificationEvent) { + protected static notification_click_event(event: NotificationEvent) { event.waitUntil(this.notification_click(event.notification)) } - override async info({ context: title, message: body, uri: data }: $mol_notify_info) { + static override async info({ context: title, message: body, uri: data }: $mol_notify_info) { const worker = this.$.$mol_service_web.worker() const tag = data const existen = await worker.registration.getNotifications({ tag }) @@ -28,7 +28,7 @@ namespace $ { } - async notification_click( notification: Notification ) { + static async notification_click( notification: Notification ) { const worker = this.$.$mol_service_web.worker() const clients = await worker.clients.matchAll({ includeUncontrolled: true, type: 'window' }) diff --git a/offline/install/install.ts b/offline/install/install.ts index e16848bd33f..59c83e6096d 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,3 +1,3 @@ namespace $ { - $mol_offline.attach() + $mol_service.attach($mol_offline) } diff --git a/offline/offline.ts b/offline/offline.ts index 2ae3a221747..9bcfe626c4e 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,17 +1,17 @@ namespace $ { - export class $mol_offline extends $mol_fetch_service { - blocked_urls = [ + export class $mol_offline extends $mol_service_plugin { + static blocked_urls = [ '//cse.google.com/adsense/search/async-ads.js' ] - override blocked( request: Request ) { + static override blocked( request: Request ) { const normalized_url = request.url.replace( /^https?:/, '' ) return this.blocked_urls.includes(normalized_url) } - override modify(request: Request) { + static override modify(request: Request) { if( request.method !== 'GET' ) return null if( !/^https?:/.test( request.url ) ) return null @@ -21,7 +21,7 @@ namespace $ { return this.respond(request) } - async respond(request: Request) { + protected static async respond(request: Request) { let fallback_header const index_html = /.+\/index\.html/.test(request.url) diff --git a/service/plugin.ts b/service/plugin.ts new file mode 100644 index 00000000000..1d48b99bfe3 --- /dev/null +++ b/service/plugin.ts @@ -0,0 +1,13 @@ +namespace $ { + export class $mol_service_plugin extends $mol_object { + static init() {} + static before_install() {} + static install() { return null as undefined | null | Promise } + static activate() { return null as undefined | null | Promise } + static state_change() {} + static message_data(data: {}) { return null as null | undefined | Promise } + + static blocked(res: Request) { return false } + static modify(res: Request) { return null as null | Response | PromiseLike } + } +} diff --git a/service/service.ts b/service/service.ts index bbf64114eb9..897a604abe2 100644 --- a/service/service.ts +++ b/service/service.ts @@ -8,28 +8,23 @@ namespace $ { @ $mol_action static send(data: {}) {} - protected static plugins = new Set<$mol_service>() + protected static plugins = new Set() - protected static add(plugin: $mol_service) { + static attach(plugin: typeof $mol_service_plugin) { this.plugins.add(plugin) + this.init() } - static attach< This extends typeof $mol_service >( - this : This, - config?: Partial< InstanceType< This > >, - ) { - const plugin = this.make(config ?? {}) + protected static init() {} - this.add(plugin) - - return plugin + static blocked_response() { + return new Response( + null, + { + status: 418, + statusText: 'Blocked' + }, + ) } - - init() {} - before_install() {} - install() { return null as undefined | null | Promise } - activate() { return null as undefined | null | Promise } - state_change() {} - message_data(data: {}) { return null as null | undefined | Promise } } } diff --git a/service/service.web.ts b/service/service.web.ts index dd93fbe7d17..92b2c5cfe91 100644 --- a/service/service.web.ts +++ b/service/service.web.ts @@ -30,9 +30,7 @@ namespace $ { protected static inited = false - protected static override add(plugin: $mol_service) { - super.add(plugin) - + protected static override init() { if (this.inited) return if ( this.in_worker() ) { @@ -45,7 +43,6 @@ namespace $ { this.inited = true } - static async registration_init() { try { @@ -92,6 +89,7 @@ namespace $ { 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)) this.inited = true for (const plugin of this.plugins) plugin.init() } @@ -155,6 +153,20 @@ namespace $ { }) } + static fetch_event(event: FetchEvent) { + const request = event.request + + for (const plugin of this.plugins) { + if (plugin.blocked(request)) { + return event.respondWith(this.blocked_response()) + } + } + + for (const plugin of this.plugins) { + const response = plugin.modify(request) + if (response) return event.respondWith(response) + } + } } $.$mol_service = $mol_service_web From 7a0cf1e8540ae91c339294ad8e6172fa148d3add Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Wed, 30 Oct 2024 10:22:24 +0300 Subject: [PATCH 29/60] $mol_service refactor detach method, auto init --- offline/install/install.ts | 3 ++- offline/offline.ts | 1 + service/service.ts | 8 ++++++-- service/service.web.ts | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/offline/install/install.ts b/offline/install/install.ts index 59c83e6096d..9145751341b 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,3 +1,4 @@ namespace $ { - $mol_service.attach($mol_offline) + // Not needed, just refer to $mol_offline anywhere + // $mol_service.attach($mol_offline) } diff --git a/offline/offline.ts b/offline/offline.ts index 9bcfe626c4e..c398dcbcd11 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -65,4 +65,5 @@ namespace $ { } } + $mol_service.attach($mol_offline) } diff --git a/service/service.ts b/service/service.ts index 897a604abe2..fd9f421f976 100644 --- a/service/service.ts +++ b/service/service.ts @@ -12,10 +12,13 @@ namespace $ { static attach(plugin: typeof $mol_service_plugin) { this.plugins.add(plugin) - this.init() } - protected static init() {} + static detach(plugin: typeof $mol_service_plugin) { + this.plugins.delete(plugin) + } + + static init() {} static blocked_response() { return new Response( @@ -27,4 +30,5 @@ namespace $ { ) } } + } diff --git a/service/service.web.ts b/service/service.web.ts index 92b2c5cfe91..b23413f477c 100644 --- a/service/service.web.ts +++ b/service/service.web.ts @@ -30,7 +30,7 @@ namespace $ { protected static inited = false - protected static override init() { + static override init() { if (this.inited) return if ( this.in_worker() ) { @@ -171,4 +171,6 @@ namespace $ { $.$mol_service = $mol_service_web + $mol_service_web.init() + } From d53c25229dd185f00aa8ba33faa0623761239d87 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Wed, 30 Oct 2024 11:19:20 +0300 Subject: [PATCH 30/60] $mol_service_plugin namespace --- notify/notify.ts | 4 +- notify/notify.web.ts | 1 - notify/service/service.ts | 2 +- notify/service/service.web.ts | 10 ++--- offline/install/install.ts | 3 +- offline/offline.ts | 4 +- service/host/host.ts | 23 ++++++++++ service/{service.web.ts => host/host.web.ts} | 45 +++++++++++--------- service/plugin.ts | 13 ------ service/service.ts | 41 +++++------------- 10 files changed, 67 insertions(+), 79 deletions(-) create mode 100644 service/host/host.ts rename service/{service.web.ts => host/host.web.ts} (79%) delete mode 100644 service/plugin.ts diff --git a/notify/notify.ts b/notify/notify.ts index c1e13a7897b..ab8284fac6f 100644 --- a/notify/notify.ts +++ b/notify/notify.ts @@ -14,11 +14,11 @@ namespace $ { } static show( info: $mol_notify_info ) { - this.$.$mol_service.send(info) + this.$.$mol_service_host.send(info) } } - $mol_service.attach($mol_notify_service) + $mol_service.$mol_notify_service } diff --git a/notify/notify.web.ts b/notify/notify.web.ts index 872d734331f..81a39b07b3f 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -28,5 +28,4 @@ namespace $ { } $.$mol_notify = $mol_notify_web - } diff --git a/notify/service/service.ts b/notify/service/service.ts index 215e984042c..330742fc79b 100644 --- a/notify/service/service.ts +++ b/notify/service/service.ts @@ -1,4 +1,4 @@ -namespace $ { +namespace $.$mol_service { export class $mol_notify_service extends $mol_service_plugin { static override message_data(data: {}) { if ('uri' in data && 'message' in data) { diff --git a/notify/service/service.web.ts b/notify/service/service.web.ts index 12adb79610b..b1e1afa79ba 100644 --- a/notify/service/service.web.ts +++ b/notify/service/service.web.ts @@ -1,7 +1,7 @@ -namespace $ { +namespace $.$mol_service { export class $mol_notify_service_web extends $mol_notify_service { static override init() { - const worker = this.$.$mol_service_web.worker() + const worker = this.$.$mol_service_host_web.worker() worker.addEventListener( 'notificationclick', this.notification_click_event.bind(this)) } @@ -10,7 +10,7 @@ namespace $ { } static override async info({ context: title, message: body, uri: data }: $mol_notify_info) { - const worker = this.$.$mol_service_web.worker() + const worker = this.$.$mol_service_host_web.worker() const tag = data const existen = await worker.registration.getNotifications({ tag }) @@ -29,7 +29,7 @@ namespace $ { } static async notification_click( notification: Notification ) { - const worker = this.$.$mol_service_web.worker() + const worker = this.$.$mol_service_host_web.worker() const clients = await worker.clients.matchAll({ includeUncontrolled: true, type: 'window' }) const last = clients.at(-1) @@ -45,5 +45,5 @@ namespace $ { } } - $.$mol_notify_service = $mol_notify_service_web + $.$mol_service.$mol_notify_service = $mol_notify_service_web } diff --git a/offline/install/install.ts b/offline/install/install.ts index 9145751341b..321c5487343 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,4 +1,3 @@ namespace $ { - // Not needed, just refer to $mol_offline anywhere - // $mol_service.attach($mol_offline) + $.$mol_service.$mol_offline } diff --git a/offline/offline.ts b/offline/offline.ts index c398dcbcd11..e646ad56ea7 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,4 +1,4 @@ -namespace $ { +namespace $.$mol_service { export class $mol_offline extends $mol_service_plugin { static blocked_urls = [ @@ -64,6 +64,4 @@ namespace $ { return clone } } - - $mol_service.attach($mol_offline) } diff --git a/service/host/host.ts b/service/host/host.ts new file mode 100644 index 00000000000..2a06aa7e0d5 --- /dev/null +++ b/service/host/host.ts @@ -0,0 +1,23 @@ +namespace $ { + export class $mol_service_host extends $mol_object { + static in_worker() { return typeof window === 'undefined' } + + static path() { return 'web.js' } + + @ $mol_action + static send(data: {}) {} + + static init() {} + + static blocked_response() { + return new Response( + null, + { + status: 418, + statusText: 'Blocked' + }, + ) + } + } + +} diff --git a/service/service.web.ts b/service/host/host.web.ts similarity index 79% rename from service/service.web.ts rename to service/host/host.web.ts index b23413f477c..c83d1199aef 100644 --- a/service/service.web.ts +++ b/service/host/host.web.ts @@ -4,7 +4,7 @@ namespace $ { export type $mol_service_reg_active = ServiceWorkerRegistration & { active: ServiceWorker } export type $mol_service_reg_installing = ServiceWorkerRegistration & { installing: ServiceWorker } - export class $mol_service_web extends $mol_service { + export class $mol_service_host_web extends $mol_service_host { static is_supported() { if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) { console.warn( 'HTTPS or localhost is required for service workers.' ) @@ -32,15 +32,13 @@ namespace $ { static override init() { if (this.inited) return + this.inited = true if ( this.in_worker() ) { - this.worker() - return + this.worker_init() + } else if ( this.is_supported() ) { + this.registration_init() } - - if ( ! this.is_supported() ) return - this.registration_init() - this.inited = true } static async registration_init() { @@ -76,25 +74,30 @@ namespace $ { worker.addEventListener( 'statechange', this.state_change.bind(this, worker)) } + static plugins = [] as (typeof $mol_service.$mol_service_plugin)[] + static state_change(worker: ServiceWorker) { for (const plugin of this.plugins) { plugin.state_change() } } - static worker() { - const worker = self as unknown as ServiceWorkerGlobalScope - if (! this.inited) { - 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)) - this.inited = true - for (const plugin of this.plugins) plugin.init() - } + static async worker_init() { + await Promise.resolve() + 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)) + + this.plugins = Object.values(this.$.$mol_service).filter(plug => plug !== $mol_service.$mol_service_plugin) - return worker + for (const plugin of this.plugins) plugin.init() + } + + static worker() { + return self as unknown as ServiceWorkerGlobalScope } protected static send_delayed = [] as {}[] @@ -169,8 +172,8 @@ namespace $ { } } - $.$mol_service = $mol_service_web + $.$mol_service_host = $mol_service_host_web - $mol_service_web.init() + $mol_service_host_web.init() } diff --git a/service/plugin.ts b/service/plugin.ts deleted file mode 100644 index 1d48b99bfe3..00000000000 --- a/service/plugin.ts +++ /dev/null @@ -1,13 +0,0 @@ -namespace $ { - export class $mol_service_plugin extends $mol_object { - static init() {} - static before_install() {} - static install() { return null as undefined | null | Promise } - static activate() { return null as undefined | null | Promise } - static state_change() {} - static message_data(data: {}) { return null as null | undefined | Promise } - - static blocked(res: Request) { return false } - static modify(res: Request) { return null as null | Response | PromiseLike } - } -} diff --git a/service/service.ts b/service/service.ts index fd9f421f976..576d726d37c 100644 --- a/service/service.ts +++ b/service/service.ts @@ -1,34 +1,13 @@ -namespace $ { - - export class $mol_service extends $mol_object { - static in_worker() { return typeof window === 'undefined' } - - static path() { return 'web.js' } - - @ $mol_action - static send(data: {}) {} - - protected static plugins = new Set() - - static attach(plugin: typeof $mol_service_plugin) { - this.plugins.add(plugin) - } - - static detach(plugin: typeof $mol_service_plugin) { - this.plugins.delete(plugin) - } - +namespace $.$mol_service { + export class $mol_service_plugin extends $mol_object { static init() {} - - static blocked_response() { - return new Response( - null, - { - status: 418, - statusText: 'Blocked' - }, - ) - } + static before_install() {} + static install() { return null as undefined | null | Promise } + static activate() { return null as undefined | null | Promise } + static state_change() {} + static message_data(data: {}) { return null as null | undefined | Promise } + + static blocked(res: Request) { return false } + static modify(res: Request) { return null as null | Response | PromiseLike } } - } From 21da91e658dcefca95694be8d453b2e976e70a87 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Wed, 30 Oct 2024 12:10:14 +0300 Subject: [PATCH 31/60] $mol_service_plugin remove plugin filter, simplify --- service/host/host.web.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/host/host.web.ts b/service/host/host.web.ts index c83d1199aef..7c507815efc 100644 --- a/service/host/host.web.ts +++ b/service/host/host.web.ts @@ -89,9 +89,9 @@ namespace $ { 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)) + worker.addEventListener( 'fetch', this.fetch_event.bind(this)) - this.plugins = Object.values(this.$.$mol_service).filter(plug => plug !== $mol_service.$mol_service_plugin) + this.plugins = Object.values(this.$.$mol_service) for (const plugin of this.plugins) plugin.init() } From cbcef3f73f46cdcf7b5b5befc8109bdac98e6f74 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Wed, 30 Oct 2024 12:22:25 +0300 Subject: [PATCH 32/60] $mol_service_plugin moved to $ namespace --- service/host/host.web.ts | 2 +- service/plugin/plugin.ts | 13 +++++++++++++ service/service.ts | 12 +----------- 3 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 service/plugin/plugin.ts diff --git a/service/host/host.web.ts b/service/host/host.web.ts index 7c507815efc..1ff0f994340 100644 --- a/service/host/host.web.ts +++ b/service/host/host.web.ts @@ -74,7 +74,7 @@ namespace $ { worker.addEventListener( 'statechange', this.state_change.bind(this, worker)) } - static plugins = [] as (typeof $mol_service.$mol_service_plugin)[] + static plugins = [] as (typeof $mol_service_plugin)[] static state_change(worker: ServiceWorker) { for (const plugin of this.plugins) { diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts new file mode 100644 index 00000000000..1d48b99bfe3 --- /dev/null +++ b/service/plugin/plugin.ts @@ -0,0 +1,13 @@ +namespace $ { + export class $mol_service_plugin extends $mol_object { + static init() {} + static before_install() {} + static install() { return null as undefined | null | Promise } + static activate() { return null as undefined | null | Promise } + static state_change() {} + static message_data(data: {}) { return null as null | undefined | Promise } + + static blocked(res: Request) { return false } + static modify(res: Request) { return null as null | Response | PromiseLike } + } +} diff --git a/service/service.ts b/service/service.ts index 576d726d37c..e7517e92b48 100644 --- a/service/service.ts +++ b/service/service.ts @@ -1,13 +1,3 @@ namespace $.$mol_service { - export class $mol_service_plugin extends $mol_object { - static init() {} - static before_install() {} - static install() { return null as undefined | null | Promise } - static activate() { return null as undefined | null | Promise } - static state_change() {} - static message_data(data: {}) { return null as null | undefined | Promise } - - static blocked(res: Request) { return false } - static modify(res: Request) { return null as null | Response | PromiseLike } - } + let _ } From ea7d08955cdf15a87587268ccdb57abdbbe63998 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Wed, 30 Oct 2024 13:19:15 +0300 Subject: [PATCH 33/60] $mol_build_client removed unused postMessage --- build/client/client.js | 1 - 1 file changed, 1 deletion(-) diff --git a/build/client/client.js b/build/client/client.js index 3f4ab24b9b7..22d91b68efe 100644 --- a/build/client/client.js +++ b/build/client/client.js @@ -10,7 +10,6 @@ function $mol_build_client() { socket.onmessage = message => { if( message.data !== '$mol_build_obsolete' ) return - postMessage('mol_build_obsolete') location.reload() } From 02d8dccb523a7694c53f29f03456e164b6a9adc2 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Thu, 31 Oct 2024 00:42:17 +0300 Subject: [PATCH 34/60] $mol_service_plugin - include mol_service, add waitUntil to modify, $mol_offline - abstract fetch to remove undici install in node --- offline/offline.ts | 12 +++++++++--- offline/offline.web.ts | 9 +++++++++ service/host/host.web.ts | 4 +++- service/plugin/plugin.ts | 8 ++++++-- 4 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 offline/offline.web.ts diff --git a/offline/offline.ts b/offline/offline.ts index e646ad56ea7..7f7ba4a3758 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -21,10 +21,16 @@ namespace $.$mol_service { return this.respond(request) } + protected static fetch(request: Request): Promise { + throw new Error('implement') + } + + static force_cache_regexp = /.+\/(index|test)\.html/ + protected static async respond(request: Request) { let fallback_header - const index_html = /.+\/index\.html/.test(request.url) + const index_html = this.force_cache_regexp.test(request.url) const cache = request.cache @@ -36,7 +42,7 @@ namespace $.$mol_service { // fetch with fallback to cache if statuses not match try { - const actual = await fetch(request) + const actual = await this.fetch(request) if (actual.status < 400) return actual throw new Error( @@ -54,7 +60,7 @@ namespace $.$mol_service { request = new Request(request, { cache: 'force-cache' }) } - const cached = await fetch(request) + const cached = await this.fetch(request) if (! fallback_header || cached.headers.get('$mol_offline_remote_status')) return cached diff --git a/offline/offline.web.ts b/offline/offline.web.ts new file mode 100644 index 00000000000..41d49670878 --- /dev/null +++ b/offline/offline.web.ts @@ -0,0 +1,9 @@ +namespace $.$mol_service { + export class $mol_offline_web extends $mol_offline { + protected static override fetch(request: Request) { + return fetch(request) + } + } + + $.$mol_service.$mol_offline = $mol_offline_web +} diff --git a/service/host/host.web.ts b/service/host/host.web.ts index 1ff0f994340..21cb76c0563 100644 --- a/service/host/host.web.ts +++ b/service/host/host.web.ts @@ -165,8 +165,10 @@ namespace $ { } } + const waitUntil = event.waitUntil.bind(event) + for (const plugin of this.plugins) { - const response = plugin.modify(request) + const response = plugin.modify(request, waitUntil) if (response) return event.respondWith(response) } } diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 1d48b99bfe3..39009d30efa 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -7,7 +7,11 @@ namespace $ { static state_change() {} static message_data(data: {}) { return null as null | undefined | Promise } - static blocked(res: Request) { return false } - static modify(res: Request) { return null as null | Response | PromiseLike } + static blocked(request: Request) { return false } + static modify(request: Request, waitUntil: (promise: Promise) => void) { + return null as null | Response | PromiseLike + } + + static service() { return this.$.$mol_service_host } } } From 7f7ce8d24c4af4d5981cb25124a085bacfe36a2f Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Thu, 31 Oct 2024 01:12:49 +0300 Subject: [PATCH 35/60] $mol_service fix inner namespace --- notify/notify.ts | 2 +- notify/service/service.ts | 2 +- notify/service/service.web.ts | 8 ++++++-- offline/install/install.ts | 2 +- offline/offline.ts | 2 +- offline/offline.web.ts | 9 +++++++-- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/notify/notify.ts b/notify/notify.ts index ab8284fac6f..fe860cb3061 100644 --- a/notify/notify.ts +++ b/notify/notify.ts @@ -19,6 +19,6 @@ namespace $ { } - $mol_service.$mol_notify_service + $mol_notify_service } diff --git a/notify/service/service.ts b/notify/service/service.ts index 330742fc79b..215e984042c 100644 --- a/notify/service/service.ts +++ b/notify/service/service.ts @@ -1,4 +1,4 @@ -namespace $.$mol_service { +namespace $ { export class $mol_notify_service extends $mol_service_plugin { static override message_data(data: {}) { if ('uri' in data && 'message' in data) { diff --git a/notify/service/service.web.ts b/notify/service/service.web.ts index b1e1afa79ba..d756678fefc 100644 --- a/notify/service/service.web.ts +++ b/notify/service/service.web.ts @@ -1,4 +1,4 @@ -namespace $.$mol_service { +namespace $ { export class $mol_notify_service_web extends $mol_notify_service { static override init() { const worker = this.$.$mol_service_host_web.worker() @@ -45,5 +45,9 @@ namespace $.$mol_service { } } - $.$mol_service.$mol_notify_service = $mol_notify_service_web + $.$mol_notify_service = $mol_notify_service_web + + export namespace $mol_service { + export const $mol_notify_service = $mol_notify_service_web + } } diff --git a/offline/install/install.ts b/offline/install/install.ts index 321c5487343..79e40bb7835 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,3 +1,3 @@ namespace $ { - $.$mol_service.$mol_offline + $.$mol_offline } diff --git a/offline/offline.ts b/offline/offline.ts index 7f7ba4a3758..35942f3deed 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,4 +1,4 @@ -namespace $.$mol_service { +namespace $ { export class $mol_offline extends $mol_service_plugin { static blocked_urls = [ diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 41d49670878..39e29e842cd 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -1,9 +1,14 @@ -namespace $.$mol_service { +namespace $ { export class $mol_offline_web extends $mol_offline { protected static override fetch(request: Request) { return fetch(request) } } - $.$mol_service.$mol_offline = $mol_offline_web + $.$mol_offline = $mol_offline_web + + export namespace $mol_service { + export const $mol_offline = $mol_offline_web + } + } From 4b10be0bd3750dd6a0c898485bf12c6fbc4ab71e Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Thu, 31 Oct 2024 11:42:56 +0300 Subject: [PATCH 36/60] $mol_service_host refactor, better update handling api --- notify/service/service.web.ts | 16 ++++---- service/host/host.web.ts | 71 ++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 35 deletions(-) diff --git a/notify/service/service.web.ts b/notify/service/service.web.ts index d756678fefc..24adefdeb8d 100644 --- a/notify/service/service.web.ts +++ b/notify/service/service.web.ts @@ -1,8 +1,8 @@ namespace $ { export class $mol_notify_service_web extends $mol_notify_service { static override init() { - const worker = this.$.$mol_service_host_web.worker() - worker.addEventListener( 'notificationclick', this.notification_click_event.bind(this)) + const scope = this.$.$mol_service_host_web.scope() + scope.addEventListener( 'notificationclick', this.notification_click_event.bind(this)) } protected static notification_click_event(event: NotificationEvent) { @@ -10,9 +10,9 @@ namespace $ { } static override async info({ context: title, message: body, uri: data }: $mol_notify_info) { - const worker = this.$.$mol_service_host_web.worker() + const scope = this.$.$mol_service_host_web.scope() const tag = data - const existen = await worker.registration.getNotifications({ tag }) + const existen = await scope.registration.getNotifications({ tag }) for( const not of existen ) { @@ -24,14 +24,14 @@ namespace $ { // const vibrate = [ 100, 200, 300, 400, 500 ] - await worker.registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) + await scope.registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) } static async notification_click( notification: Notification ) { - const worker = this.$.$mol_service_host_web.worker() + const scope = this.$.$mol_service_host_web.scope() - const clients = await worker.clients.matchAll({ includeUncontrolled: true, type: 'window' }) + const clients = await scope.clients.matchAll({ includeUncontrolled: true, type: 'window' }) const last = clients.at(-1) if( last ) { @@ -41,7 +41,7 @@ namespace $ { return } - await worker.clients.openWindow( notification.data ) + await scope.clients.openWindow( notification.data ) } } diff --git a/service/host/host.web.ts b/service/host/host.web.ts index 21cb76c0563..43ba799adfc 100644 --- a/service/host/host.web.ts +++ b/service/host/host.web.ts @@ -1,9 +1,6 @@ /// namespace $ { - export type $mol_service_reg_active = ServiceWorkerRegistration & { active: ServiceWorker } - export type $mol_service_reg_installing = ServiceWorkerRegistration & { installing: ServiceWorker } - export class $mol_service_host_web extends $mol_service_host { static is_supported() { if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) { @@ -26,7 +23,14 @@ namespace $ { } } - protected static registration = null as null | $mol_service_reg_active + protected static _registration = null as null | ServiceWorkerRegistration + + static registration() { + if (this.in_worker()) return this.scope().registration + if (! this._registration) throw new Error('Access before registration_init') + + return this._registration + } protected static inited = false @@ -41,17 +45,26 @@ namespace $ { } } + static plugins = [] as (typeof $mol_service_plugin)[] + static async registration_init() { + const navigator = this.$.$mol_dom_context.navigator + try { const reg_promise = navigator.serviceWorker.register(this.path()) - const ready = navigator.serviceWorker.ready as Promise<$mol_service_reg_active> - const reg = (await reg_promise) as $mol_service_reg_active - if (reg.installing) this.installing(reg as $mol_service_reg_installing) + const ready = navigator.serviceWorker.ready + const reg = await reg_promise - await ready + this._registration = reg + + if (reg.waiting) this.state_change(reg.waiting) + else if (reg.installing) this.update_found() + else { + reg.addEventListener( 'updatefound', this.update_found.bind(this)) + } - this.registration = reg + await ready for (const data of this.send_delayed) { this.send(data) @@ -66,15 +79,19 @@ namespace $ { } } - static installing(reg: $mol_service_reg_installing) { - reg.addEventListener( 'updatefound', this.update_found.bind(this, reg.installing)) - } - - static update_found(worker: ServiceWorker) { + static update_found() { + const worker = this.registration().installing + if (! worker) throw new Error('No installing worker in updatefound event') worker.addEventListener( 'statechange', this.state_change.bind(this, worker)) } - static plugins = [] as (typeof $mol_service_plugin)[] + static worker() { + const reg = this.registration() + const worker = reg.installing ?? reg.waiting ?? reg.active + if (! worker) throw new Error('No worker available in registration') + + return worker + } static state_change(worker: ServiceWorker) { for (const plugin of this.plugins) { @@ -84,19 +101,19 @@ namespace $ { static async worker_init() { await Promise.resolve() - 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)) + const scope = this.scope() + scope.addEventListener( 'beforeinstallprompt' , this.before_install.bind(this) ) + scope.addEventListener( 'install' , this.install.bind(this)) + scope.addEventListener( 'activate' , this.activate.bind(this)) + scope.addEventListener( 'message', this.message.bind(this)) + scope.addEventListener( 'fetch', this.fetch_event.bind(this)) this.plugins = Object.values(this.$.$mol_service) for (const plugin of this.plugins) plugin.init() } - static worker() { + static scope() { return self as unknown as ServiceWorkerGlobalScope } @@ -104,10 +121,10 @@ namespace $ { @ $mol_action static override send(data: {}) { - const active = this.registration?.active + const worker = this.worker() - if (active) { - active.postMessage(data) + if (worker) { + worker.postMessage(data) } else { this.send_delayed.push(data) } @@ -139,7 +156,7 @@ namespace $ { if (result) event.waitUntil(result) } - this.worker().skipWaiting() + this.scope().skipWaiting() } static activate(event: ExtendableEvent) { @@ -148,7 +165,7 @@ namespace $ { if (result) event.waitUntil(result) } - event.waitUntil( this.worker().clients.claim() ) + event.waitUntil( this.scope().clients.claim() ) this.$.$mol_log3_done({ place: `${this}.activate()`, From d12f71f92d3100fbd3aedb620b14a7302c52268b Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Fri, 1 Nov 2024 18:16:35 +0300 Subject: [PATCH 37/60] $mol_service review fixes --- notify/notify.ts | 14 ++- notify/notify.web.ts | 53 +++++++++++ notify/service/service.ts | 14 --- notify/service/service.web.ts | 53 ----------- offline/offline.web.ts | 4 + service/plugin/plugin.ts | 4 +- service/prepare/event.ts | 10 ++ service/{host/host.ts => worker/worker.ts} | 4 +- .../host.web.ts => worker/worker.web.ts} | 93 ++++++++++++------- 9 files changed, 144 insertions(+), 105 deletions(-) delete mode 100644 notify/service/service.ts delete mode 100644 notify/service/service.web.ts create mode 100644 service/prepare/event.ts rename service/{host/host.ts => worker/worker.ts} (66%) rename service/{host/host.web.ts => worker/worker.web.ts} (69%) diff --git a/notify/notify.ts b/notify/notify.ts index fe860cb3061..c352c4626f6 100644 --- a/notify/notify.ts +++ b/notify/notify.ts @@ -14,11 +14,21 @@ namespace $ { } static show( info: $mol_notify_info ) { - this.$.$mol_service_host.send(info) + this.$.$mol_service_worker.send(info) } } - $mol_notify_service + export class $mol_notify_service extends $mol_service_plugin { + static override message_data(data: {}) { + if ('uri' in data && 'message' in data) { + this.show(data as $mol_notify_info) + } + return null + } + + static async show(info: $mol_notify_info) {} + + } } diff --git a/notify/notify.web.ts b/notify/notify.web.ts index 81a39b07b3f..615e5d0f0f6 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -28,4 +28,57 @@ namespace $ { } $.$mol_notify = $mol_notify_web + + export class $mol_notify_service_web extends $mol_notify_service { + static override init() { + const scope = this.$.$mol_service_worker_web.scope() + scope.addEventListener( 'notificationclick', this.notification_click_event.bind(this)) + } + + protected static notification_click_event(event: NotificationEvent) { + event.waitUntil(this.notification_click(event.notification)) + } + + static override async show({ context: title, message: body, uri: data }: $mol_notify_info) { + const scope = this.$.$mol_service_worker_web.scope() + const tag = data + const existen = await scope.registration.getNotifications({ tag }) + + for( const not of existen ) { + + if( not.body.indexOf( body ) !== -1 ) body = not.body + else if( body.indexOf( not.body ) === -1 ) body = not.body + '\n' + body + + not.close() + } + + // const vibrate = [ 100, 200, 300, 400, 500 ] + + await scope.registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) + + } + + static async notification_click( notification: Notification ) { + const scope = this.$.$mol_service_worker_web.scope() + + const clients = await scope.clients.matchAll({ includeUncontrolled: true, type: 'window' }) + const last = clients.at(-1) + + if( last ) { + await last.focus() + await last.navigate( notification.data ) + + return + } + + await scope.clients.openWindow( notification.data ) + } + } + + $.$mol_notify_service = $mol_notify_service_web + + export namespace $mol_service { + export const $mol_notify_service = $mol_notify_service_web + } + } diff --git a/notify/service/service.ts b/notify/service/service.ts deleted file mode 100644 index 215e984042c..00000000000 --- a/notify/service/service.ts +++ /dev/null @@ -1,14 +0,0 @@ -namespace $ { - export class $mol_notify_service extends $mol_service_plugin { - static override message_data(data: {}) { - if ('uri' in data && 'message' in data) { - this.info(data as $mol_notify_info) - } - return null - } - - static async info(info: $mol_notify_info) {} - - } - -} diff --git a/notify/service/service.web.ts b/notify/service/service.web.ts deleted file mode 100644 index 24adefdeb8d..00000000000 --- a/notify/service/service.web.ts +++ /dev/null @@ -1,53 +0,0 @@ -namespace $ { - export class $mol_notify_service_web extends $mol_notify_service { - static override init() { - const scope = this.$.$mol_service_host_web.scope() - scope.addEventListener( 'notificationclick', this.notification_click_event.bind(this)) - } - - protected static notification_click_event(event: NotificationEvent) { - event.waitUntil(this.notification_click(event.notification)) - } - - static override async info({ context: title, message: body, uri: data }: $mol_notify_info) { - const scope = this.$.$mol_service_host_web.scope() - const tag = data - const existen = await scope.registration.getNotifications({ tag }) - - for( const not of existen ) { - - if( not.body.indexOf( body ) !== -1 ) body = not.body - else if( body.indexOf( not.body ) === -1 ) body = not.body + '\n' + body - - not.close() - } - - // const vibrate = [ 100, 200, 300, 400, 500 ] - - await scope.registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) - - } - - static async notification_click( notification: Notification ) { - const scope = this.$.$mol_service_host_web.scope() - - const clients = await scope.clients.matchAll({ includeUncontrolled: true, type: 'window' }) - const last = clients.at(-1) - - if( last ) { - await last.focus() - await last.navigate( notification.data ) - - return - } - - await scope.clients.openWindow( notification.data ) - } - } - - $.$mol_notify_service = $mol_notify_service_web - - export namespace $mol_service { - export const $mol_notify_service = $mol_notify_service_web - } -} diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 39e29e842cd..4100598d22c 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -3,6 +3,10 @@ namespace $ { protected static override fetch(request: Request) { return fetch(request) } + + static override activate() { + return this.$.$mol_service_worker_web.scope().clients.claim() + } } $.$mol_offline = $mol_offline_web diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 39009d30efa..28d944c6c9c 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -1,7 +1,7 @@ namespace $ { export class $mol_service_plugin extends $mol_object { static init() {} - static before_install() {} + static prepare(event: $mol_service_prepare_event) { return null as null | undefined | boolean } static install() { return null as undefined | null | Promise } static activate() { return null as undefined | null | Promise } static state_change() {} @@ -12,6 +12,6 @@ namespace $ { return null as null | Response | PromiseLike } - static service() { return this.$.$mol_service_host } + static service() { return this.$.$mol_service_worker } } } diff --git a/service/prepare/event.ts b/service/prepare/event.ts new file mode 100644 index 00000000000..d1bc9c53e9a --- /dev/null +++ b/service/prepare/event.ts @@ -0,0 +1,10 @@ +namespace $ { + export interface $mol_service_prepare_event { + preventDefault(): void + prompt(): Promise + platforms: readonly string[] + userChoise: Promise<{ + outcome: 'accepted' | 'dismissed' + }> + } +} diff --git a/service/host/host.ts b/service/worker/worker.ts similarity index 66% rename from service/host/host.ts rename to service/worker/worker.ts index 2a06aa7e0d5..92f19584ad5 100644 --- a/service/host/host.ts +++ b/service/worker/worker.ts @@ -1,6 +1,6 @@ namespace $ { - export class $mol_service_host extends $mol_object { - static in_worker() { return typeof window === 'undefined' } + export class $mol_service_worker extends $mol_object { + protected static in_worker() { return typeof window === 'undefined' } static path() { return 'web.js' } diff --git a/service/host/host.web.ts b/service/worker/worker.web.ts similarity index 69% rename from service/host/host.web.ts rename to service/worker/worker.web.ts index 43ba799adfc..38a4b60b017 100644 --- a/service/host/host.web.ts +++ b/service/worker/worker.web.ts @@ -1,7 +1,8 @@ /// namespace $ { - export class $mol_service_host_web extends $mol_service_host { + + export class $mol_service_worker_web extends $mol_service_worker { static is_supported() { if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) { console.warn( 'HTTPS or localhost is required for service workers.' ) @@ -32,12 +33,7 @@ namespace $ { return this._registration } - protected static inited = false - static override init() { - if (this.inited) return - this.inited = true - if ( this.in_worker() ) { this.worker_init() } else if ( this.is_supported() ) { @@ -48,6 +44,7 @@ namespace $ { static plugins = [] as (typeof $mol_service_plugin)[] static async registration_init() { + window.addEventListener( 'beforeinstallprompt' , this.prepare.bind(this) as unknown as (e: Event) => unknown ) const navigator = this.$.$mol_dom_context.navigator @@ -102,15 +99,25 @@ namespace $ { static async worker_init() { await Promise.resolve() const scope = this.scope() - scope.addEventListener( 'beforeinstallprompt' , this.before_install.bind(this) ) scope.addEventListener( 'install' , this.install.bind(this)) scope.addEventListener( 'activate' , this.activate.bind(this)) scope.addEventListener( 'message', this.message.bind(this)) - scope.addEventListener( 'fetch', this.fetch_event.bind(this)) + scope.addEventListener( 'fetch', this.fetch.bind(this)) this.plugins = Object.values(this.$.$mol_service) - for (const plugin of this.plugins) plugin.init() + for (const plugin of this.plugins) { + try { + plugin.init() + } catch (error) { + this.log_error(plugin, error) + } + } + } + + protected static log_error(plugin: typeof $mol_service_plugin, error: unknown) { + ;(error as Error).message = `${plugin.toString()}: ${(error as Error).message}` + console.error(error) } static scope() { @@ -121,6 +128,7 @@ namespace $ { @ $mol_action static override send(data: {}) { + if ( this.in_worker() ) return const worker = this.worker() if (worker) { @@ -130,6 +138,17 @@ namespace $ { } } + static prepare(event: $mol_service_prepare_event) { + for (const plugin of this.plugins) { + try { + if ( plugin.prepare(event) ) return + } catch (error) { + this.log_error(plugin, error) + } + } + + } + static message(event: ExtendableMessageEvent) { const data = event.data as string | null | { [k: string]: unknown @@ -137,23 +156,23 @@ namespace $ { if ( ! data || typeof data !== 'object' ) return false for (const plugin of this.plugins) { - const result = plugin.message_data(data) - if (result) event.waitUntil(result) - } - } - - static before_install(event: Event & { prompt?(): void }) { - for (const plugin of this.plugins) { - plugin.before_install() + try { + const result = plugin.message_data(data) + if (result) event.waitUntil(result) + } catch (error) { + this.log_error(plugin, error) + } } - - event.prompt?.() } static install(event: ExtendableEvent) { for (const plugin of this.plugins) { - const result = plugin.install() - if (result) event.waitUntil(result) + try { + const result = plugin.install() + if (result) event.waitUntil(result) + } catch (error) { + this.log_error(plugin, error) + } } this.scope().skipWaiting() @@ -161,38 +180,48 @@ namespace $ { static activate(event: ExtendableEvent) { for (const plugin of this.plugins) { - const result = plugin.activate() - if (result) event.waitUntil(result) + try { + const result = plugin.activate() + if (result) event.waitUntil(result) + } catch (error) { + this.log_error(plugin, error) + } } - event.waitUntil( this.scope().clients.claim() ) - this.$.$mol_log3_done({ place: `${this}.activate()`, message: 'Activated', }) } - static fetch_event(event: FetchEvent) { + static fetch(event: FetchEvent) { const request = event.request for (const plugin of this.plugins) { - if (plugin.blocked(request)) { - return event.respondWith(this.blocked_response()) + try { + if (plugin.blocked(request)) { + return event.respondWith(this.blocked_response()) + } + } catch (error) { + this.log_error(plugin, error) } } const waitUntil = event.waitUntil.bind(event) for (const plugin of this.plugins) { - const response = plugin.modify(request, waitUntil) - if (response) return event.respondWith(response) + try { + const response = plugin.modify(request, waitUntil) + if (response) return event.respondWith(response) + } catch (error) { + this.log_error(plugin, error) + } } } } - $.$mol_service_host = $mol_service_host_web + $.$mol_service_worker = $mol_service_worker_web - $mol_service_host_web.init() + $mol_service_worker_web.init() } From a202b1bc0bdb55f21d5e15a71907b99fd62baf73 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Fri, 1 Nov 2024 18:28:22 +0300 Subject: [PATCH 38/60] $mol_offline regexp to URL parser --- offline/offline.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/offline/offline.ts b/offline/offline.ts index 35942f3deed..74fcf8516f5 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -25,16 +25,15 @@ namespace $ { throw new Error('implement') } - static force_cache_regexp = /.+\/(index|test)\.html/ - protected static async respond(request: Request) { let fallback_header - const index_html = this.force_cache_regexp.test(request.url) + const url = new URL(request.url) + const html = url.pathname.endsWith('.html') const cache = request.cache - if (cache === 'reload' || ( cache === 'no-cache' && ! index_html ) ) { + if (cache === 'reload' || ( cache === 'no-cache' && ! html ) ) { if (cache === 'reload') { // F5 + Disable cache request = new Request(request, { cache: 'no-cache' }) From 61fb6db57513f471d5316ea03cc59be2c4f73ded Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Fri, 1 Nov 2024 21:13:20 +0300 Subject: [PATCH 39/60] $mol_service separate worker plugins, fixes --- notify/notify.ts | 2 +- notify/notify.web.ts | 25 +++---- offline/offline.ts | 2 +- offline/offline.web.ts | 2 +- service/plugin/plugin.ts | 11 +-- service/worker/worker.ts | 12 +++- service/worker/worker.web.ts | 126 ++++++++++++++++++++++------------- 7 files changed, 107 insertions(+), 73 deletions(-) diff --git a/notify/notify.ts b/notify/notify.ts index c352c4626f6..8d20b626dd2 100644 --- a/notify/notify.ts +++ b/notify/notify.ts @@ -19,7 +19,7 @@ namespace $ { } - export class $mol_notify_service extends $mol_service_plugin { + export class $mol_notify_service extends $mol_service_plugin_notify { static override message_data(data: {}) { if ('uri' in data && 'message' in data) { this.show(data as $mol_notify_info) diff --git a/notify/notify.web.ts b/notify/notify.web.ts index 615e5d0f0f6..3120eacf8a1 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -30,19 +30,10 @@ namespace $ { $.$mol_notify = $mol_notify_web export class $mol_notify_service_web extends $mol_notify_service { - static override init() { - const scope = this.$.$mol_service_worker_web.scope() - scope.addEventListener( 'notificationclick', this.notification_click_event.bind(this)) - } - - protected static notification_click_event(event: NotificationEvent) { - event.waitUntil(this.notification_click(event.notification)) - } - static override async show({ context: title, message: body, uri: data }: $mol_notify_info) { - const scope = this.$.$mol_service_worker_web.scope() + const registration = this.$.$mol_service_worker_web.registration() const tag = data - const existen = await scope.registration.getNotifications({ tag }) + const existen = await registration.getNotifications({ tag }) for( const not of existen ) { @@ -54,15 +45,15 @@ namespace $ { // const vibrate = [ 100, 200, 300, 400, 500 ] - await scope.registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) + await registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) } - static async notification_click( notification: Notification ) { - const scope = this.$.$mol_service_worker_web.scope() + static override async notification( notification: Notification ) { + const clients = this.$.$mol_service_worker_web.clients() - const clients = await scope.clients.matchAll({ includeUncontrolled: true, type: 'window' }) - const last = clients.at(-1) + const matched = await clients.matchAll({ includeUncontrolled: true, type: 'window' }) + const last = matched.at(-1) if( last ) { await last.focus() @@ -71,7 +62,7 @@ namespace $ { return } - await scope.clients.openWindow( notification.data ) + await clients.openWindow( notification.data ) } } diff --git a/offline/offline.ts b/offline/offline.ts index 74fcf8516f5..9b6116da3a3 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -1,6 +1,6 @@ namespace $ { - export class $mol_offline extends $mol_service_plugin { + export class $mol_offline extends $mol_service_plugin_cache { static blocked_urls = [ '//cse.google.com/adsense/search/async-ads.js' ] diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 4100598d22c..53b1bd26361 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -5,7 +5,7 @@ namespace $ { } static override activate() { - return this.$.$mol_service_worker_web.scope().clients.claim() + return this.$.$mol_service_worker_web.clients().claim() } } diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 28d944c6c9c..84dc849bcf9 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -1,17 +1,20 @@ namespace $ { export class $mol_service_plugin extends $mol_object { - static init() {} - static prepare(event: $mol_service_prepare_event) { return null as null | undefined | boolean } static install() { return null as undefined | null | Promise } static activate() { return null as undefined | null | Promise } - static state_change() {} static message_data(data: {}) { return null as null | undefined | Promise } + static service() { return this.$.$mol_service_worker } + } + + export class $mol_service_plugin_cache extends $mol_service_plugin { static blocked(request: Request) { return false } static modify(request: Request, waitUntil: (promise: Promise) => void) { return null as null | Response | PromiseLike } + } - static service() { return this.$.$mol_service_worker } + export class $mol_service_plugin_notify extends $mol_service_plugin { + static notification(e: unknown) { return null as null | undefined | Promise } } } diff --git a/service/worker/worker.ts b/service/worker/worker.ts index 92f19584ad5..5902b75f9a0 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -1,13 +1,19 @@ namespace $ { export class $mol_service_worker extends $mol_object { - protected static in_worker() { return typeof window === 'undefined' } - static path() { return 'web.js' } @ $mol_action static send(data: {}) {} - static init() {} + @ $mol_mem + static prepare(next?: $mol_service_prepare_event) { + return next ? next : null + } + + @ $mol_mem + static prepare_choise() { return $mol_wire_sync(this).choise_promise()?.outcome ?? null } + + static choise_promise() { return this.prepare()?.userChoise } static blocked_response() { return new Response( diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index 38a4b60b017..44e697c9faa 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -3,7 +3,7 @@ namespace $ { export class $mol_service_worker_web extends $mol_service_worker { - static is_supported() { + protected static is_supported() { if( location.protocol !== 'https:' && location.hostname !== 'localhost' ) { console.warn( 'HTTPS or localhost is required for service workers.' ) return false @@ -17,12 +17,7 @@ namespace $ { return true } - static handler< E extends ExtendableEvent >( handle : ( event: E )=> null | undefined | Promise ) { - return ( event: E )=> { - const result = handle( event ) - if (result) event.waitUntil( result ) - } - } + protected static in_worker() { return typeof window === 'undefined' } protected static _registration = null as null | ServiceWorkerRegistration @@ -33,7 +28,7 @@ namespace $ { return this._registration } - static override init() { + static init() { if ( this.in_worker() ) { this.worker_init() } else if ( this.is_supported() ) { @@ -41,7 +36,11 @@ namespace $ { } } - static plugins = [] as (typeof $mol_service_plugin)[] + static plugins = [] as ( + typeof $mol_service_plugin + | typeof $mol_service_plugin_notify + | typeof $mol_service_plugin_cache + )[] static async registration_init() { window.addEventListener( 'beforeinstallprompt' , this.prepare.bind(this) as unknown as (e: Event) => unknown ) @@ -55,7 +54,7 @@ namespace $ { this._registration = reg - if (reg.waiting) this.state_change(reg.waiting) + if (reg.waiting) this.state(null) else if (reg.installing) this.update_found() else { reg.addEventListener( 'updatefound', this.update_found.bind(this)) @@ -76,25 +75,18 @@ namespace $ { } } - static update_found() { - const worker = this.registration().installing - if (! worker) throw new Error('No installing worker in updatefound event') - worker.addEventListener( 'statechange', this.state_change.bind(this, worker)) + protected static update_found() { + const worker = this.registration().installing! + worker.addEventListener( 'statechange', e => this.state(null)) } - static worker() { + protected static worker() { const reg = this.registration() - const worker = reg.installing ?? reg.waiting ?? reg.active - if (! worker) throw new Error('No worker available in registration') - - return worker + return reg.installing ?? reg.waiting ?? reg.active } - static state_change(worker: ServiceWorker) { - for (const plugin of this.plugins) { - plugin.state_change() - } - } + @ $mol_mem + static state(reset?: null) { return this.worker()?.state ?? null } static async worker_init() { await Promise.resolve() @@ -103,16 +95,9 @@ namespace $ { scope.addEventListener( 'activate' , this.activate.bind(this)) scope.addEventListener( 'message', this.message.bind(this)) scope.addEventListener( 'fetch', this.fetch.bind(this)) + scope.addEventListener( 'notificationclick', this.notification_click.bind(this)) this.plugins = Object.values(this.$.$mol_service) - - for (const plugin of this.plugins) { - try { - plugin.init() - } catch (error) { - this.log_error(plugin, error) - } - } } protected static log_error(plugin: typeof $mol_service_plugin, error: unknown) { @@ -120,33 +105,64 @@ namespace $ { console.error(error) } - static scope() { + protected static scope() { return self as unknown as ServiceWorkerGlobalScope } + static clients() { + return this.scope().clients + } + protected static send_delayed = [] as {}[] @ $mol_action static override send(data: {}) { - if ( this.in_worker() ) return - const worker = this.worker() + const worker = this.in_worker() ? this.worker() : null if (worker) { - worker.postMessage(data) - } else { - this.send_delayed.push(data) + try { + worker.postMessage(data) + } catch (error) { + console.error(error) + } + + return + } + + this.send_delayed.push(data) + + if (this.in_worker() && ! this.send_clients_promise) { + this.send_clients_promise = this.send_clients_async() } } - static prepare(event: $mol_service_prepare_event) { - for (const plugin of this.plugins) { - try { - if ( plugin.prepare(event) ) return - } catch (error) { - this.log_error(plugin, error) + protected static send_clients_promise = null as null | Promise + + protected static async send_clients_async() { + + let clients = [] as readonly WindowClient[] + + try { + clients = await this.clients().matchAll({ includeUncontrolled: true, type: 'window' }) + } catch (error) { + console.error(error) + } + + for (const client of clients) { + for (const data of this.send_delayed) { + try { + client.postMessage(data) + } catch (error) { + console.error(error) + } } } + this.send_delayed = [] + this.send_clients_promise = null + + + return null } static message(event: ExtendableMessageEvent) { @@ -194,12 +210,28 @@ namespace $ { }) } + static notification_click(event: NotificationEvent) { + for (const plugin of this.plugins) { + if ( ! ( plugin.prototype instanceof $mol_service_plugin_notify ) ) continue + + try { + const result = (plugin as typeof $mol_service_plugin_notify).notification(event.notification) + if ( result ) return event.waitUntil(result) + } catch (error) { + this.log_error(plugin, error) + } + } + + } + static fetch(event: FetchEvent) { const request = event.request for (const plugin of this.plugins) { + if ( ! ( plugin.prototype instanceof $mol_service_plugin_cache ) ) continue + try { - if (plugin.blocked(request)) { + if ((plugin as typeof $mol_service_plugin_cache).blocked(request)) { return event.respondWith(this.blocked_response()) } } catch (error) { @@ -210,8 +242,10 @@ namespace $ { const waitUntil = event.waitUntil.bind(event) for (const plugin of this.plugins) { + if ( ! ( plugin.prototype instanceof $mol_service_plugin_cache ) ) continue + try { - const response = plugin.modify(request, waitUntil) + const response = (plugin as typeof $mol_service_plugin_cache).modify(request, waitUntil) if (response) return event.respondWith(response) } catch (error) { this.log_error(plugin, error) From 5b68802e39decc945d2f3f0fd17a8b740f1bc0d8 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 2 Nov 2024 10:55:31 +0300 Subject: [PATCH 40/60] $mol_service moved to wires, better errors --- notify/notify.ts | 3 +- notify/notify.web.ts | 21 +- offline/offline.ts | 13 +- offline/offline.web.ts | 6 +- service/message/event/event.ts | 11 ++ service/message/event/event.web.ts | 25 +++ service/plugin/plugin.ts | 10 +- service/worker/worker.ts | 4 +- service/worker/worker.web.ts | 303 ++++++++++++++++------------- 9 files changed, 237 insertions(+), 159 deletions(-) create mode 100644 service/message/event/event.ts create mode 100644 service/message/event/event.web.ts diff --git a/notify/notify.ts b/notify/notify.ts index 8d20b626dd2..59d652bceda 100644 --- a/notify/notify.ts +++ b/notify/notify.ts @@ -23,11 +23,12 @@ namespace $ { static override message_data(data: {}) { if ('uri' in data && 'message' in data) { this.show(data as $mol_notify_info) + return true } return null } - static async show(info: $mol_notify_info) {} + static show(info: $mol_notify_info) {} } diff --git a/notify/notify.web.ts b/notify/notify.web.ts index 3120eacf8a1..30abdc78345 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -30,10 +30,10 @@ namespace $ { $.$mol_notify = $mol_notify_web export class $mol_notify_service_web extends $mol_notify_service { - static override async show({ context: title, message: body, uri: data }: $mol_notify_info) { + static override show({ context: title, message: body, uri: data }: $mol_notify_info) { const registration = this.$.$mol_service_worker_web.registration() const tag = data - const existen = await registration.getNotifications({ tag }) + const existen = registration.getNotifications({ tag }) for( const not of existen ) { @@ -45,24 +45,23 @@ namespace $ { // const vibrate = [ 100, 200, 300, 400, 500 ] - await registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) + registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) } - static override async notification( notification: Notification ) { - const clients = this.$.$mol_service_worker_web.clients() - - const matched = await clients.matchAll({ includeUncontrolled: true, type: 'window' }) + static override notification( notification: Notification ) { + const matched = this.$.$mol_service_worker_web.clients_filter({ includeUncontrolled: true, type: 'window' }) const last = matched.at(-1) if( last ) { - await last.focus() - await last.navigate( notification.data ) + last.focus() + last.navigate( notification.data ) - return + return null } - await clients.openWindow( notification.data ) + this.$.$mol_service_worker_web.window_open( notification.data ) + return null } } diff --git a/offline/offline.ts b/offline/offline.ts index 9b6116da3a3..6e4fff3dd29 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -21,11 +21,11 @@ namespace $ { return this.respond(request) } - protected static fetch(request: Request): Promise { - throw new Error('implement') + protected static fetch(request: Request) { + return null as null | Response } - protected static async respond(request: Request) { + protected static respond(request: Request) { let fallback_header const url = new URL(request.url) @@ -41,7 +41,8 @@ namespace $ { // fetch with fallback to cache if statuses not match try { - const actual = await this.fetch(request) + const actual = this.fetch(request) + if (! actual) return null if (actual.status < 400) return actual throw new Error( @@ -59,8 +60,8 @@ namespace $ { request = new Request(request, { cache: 'force-cache' }) } - const cached = await this.fetch(request) - + const cached = this.fetch(request) + if (! cached) return null if (! fallback_header || cached.headers.get('$mol_offline_remote_status')) return cached const clone = new Response(cached.body, cached) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 53b1bd26361..0127e621371 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -1,11 +1,15 @@ namespace $ { export class $mol_offline_web extends $mol_offline { protected static override fetch(request: Request) { + return $mol_wire_sync(this).fetch_async(request) + } + + static fetch_async(request: Request) { return fetch(request) } static override activate() { - return this.$.$mol_service_worker_web.clients().claim() + return this.$.$mol_service_worker_web.claim() } } diff --git a/service/message/event/event.ts b/service/message/event/event.ts new file mode 100644 index 00000000000..0f58e7df913 --- /dev/null +++ b/service/message/event/event.ts @@ -0,0 +1,11 @@ +namespace $ { + export class $mol_service_message_event extends $mol_object { + data() { + return null as null | Record + } + + result(result: {} | null) {} + + error(error: Error) {} + } +} diff --git a/service/message/event/event.web.ts b/service/message/event/event.web.ts new file mode 100644 index 00000000000..dd2d9510bdf --- /dev/null +++ b/service/message/event/event.web.ts @@ -0,0 +1,25 @@ +namespace $ { + export class $mol_service_message_event_web extends $mol_service_message_event { + event!: ExtendableMessageEvent + + override data() { + const data = this.event.data as string | null | { + [k: string]: unknown + } + if ( ! data || typeof data !== 'object' ) return null + return data + } + + @ $mol_action + override result(result: {} | null) { + this.event.ports[0].postMessage({ error: null, result }) + } + + @ $mol_action + override error(error: Error) { + this.event.ports[0].postMessage({ error: error.toString(), result: null }) + } + } + + $.$mol_service_message_event = $mol_service_message_event_web +} diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 84dc849bcf9..55357c7dd5a 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -1,8 +1,8 @@ namespace $ { export class $mol_service_plugin extends $mol_object { - static install() { return null as undefined | null | Promise } - static activate() { return null as undefined | null | Promise } - static message_data(data: {}) { return null as null | undefined | Promise } + static install() { return null as unknown } + static activate() { return null as unknown } + static message_data(data: {}) { return null as unknown } static service() { return this.$.$mol_service_worker } } @@ -10,11 +10,11 @@ namespace $ { export class $mol_service_plugin_cache extends $mol_service_plugin { static blocked(request: Request) { return false } static modify(request: Request, waitUntil: (promise: Promise) => void) { - return null as null | Response | PromiseLike + return null as null | Response } } export class $mol_service_plugin_notify extends $mol_service_plugin { - static notification(e: unknown) { return null as null | undefined | Promise } + static notification(e: unknown) { return null as unknown } } } diff --git a/service/worker/worker.ts b/service/worker/worker.ts index 5902b75f9a0..5d99b35c6f5 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -2,8 +2,10 @@ namespace $ { export class $mol_service_worker extends $mol_object { static path() { return 'web.js' } + static send_timeout() { return 20000 } + @ $mol_action - static send(data: {}) {} + static send(data: {}) { return null as unknown } @ $mol_mem static prepare(next?: $mol_service_prepare_event) { diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index 44e697c9faa..39d81f8ab58 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -3,76 +3,60 @@ namespace $ { export class $mol_service_worker_web extends $mol_service_worker { - protected 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 in_worker() { return typeof window === 'undefined' } - protected static _registration = null as null | ServiceWorkerRegistration - - static registration() { - if (this.in_worker()) return this.scope().registration - if (! this._registration) throw new Error('Access before registration_init') - - return this._registration - } - + @ $mol_mem static init() { - if ( this.in_worker() ) { - this.worker_init() - } else if ( this.is_supported() ) { - this.registration_init() + $mol_wire_solid() + try { + if ( this.in_worker() ) this.scope_inited() + else this.registration_inited() + } catch (error) { + this.$.$mol_fail_log(error) } } - static plugins = [] as ( - typeof $mol_service_plugin - | typeof $mol_service_plugin_notify - | typeof $mol_service_plugin_cache - )[] - - static async registration_init() { - window.addEventListener( 'beforeinstallprompt' , this.prepare.bind(this) as unknown as (e: Event) => unknown ) - - const navigator = this.$.$mol_dom_context.navigator + @ $mol_mem + static container() { + const win = this.$.$mol_dom_context - try { - const reg_promise = navigator.serviceWorker.register(this.path()) - const ready = navigator.serviceWorker.ready - const reg = await reg_promise + if( ! win.navigator.serviceWorker ) { + throw new Error('Service Worker is not supported.') + } + if( win.location.protocol !== 'https:' && win.location.hostname !== 'localhost' ) { + throw new Error( 'HTTPS or localhost is required for service workers.' ) + } - this._registration = reg + win.addEventListener( 'beforeinstallprompt' , this.prepare.bind(this) as unknown as (e: Event) => unknown ) - if (reg.waiting) this.state(null) - else if (reg.installing) this.update_found() - else { - reg.addEventListener( 'updatefound', this.update_found.bind(this)) - } + return $mol_wire_sync(win.navigator.serviceWorker) + } - await ready + @ $mol_mem + static registration() { + $mol_wire_solid() + const reg = this.in_worker() ? this.scope().registration : this.container().register( this.path() ) + return $mol_wire_sync(reg) + } - for (const data of this.send_delayed) { - this.send(data) - } + @ $mol_mem + static registration_inited() { + const reg = this.registration() + if (reg.waiting) this.state(null) + else if (reg.installing) this.update_found() + else { + reg.addEventListener( 'updatefound', this.update_found.bind(this)) + } + return reg + } - this.send_delayed = [] + static registration_ready_async() { return this.container().ready } - return true - } catch (error) { - console.error(error) - return false - } + @ $mol_mem + static registration_ready() { + $mol_wire_solid() + this.registration_inited() + return $mol_wire_sync(this).registration_ready_async() } protected static update_found() { @@ -88,119 +72,168 @@ namespace $ { @ $mol_mem static state(reset?: null) { return this.worker()?.state ?? null } - static async worker_init() { - await Promise.resolve() + @ $mol_mem + static scope_inited() { const scope = this.scope() - scope.addEventListener( 'install' , this.install.bind(this)) - scope.addEventListener( 'activate' , this.activate.bind(this)) - scope.addEventListener( 'message', this.message.bind(this)) - scope.addEventListener( 'fetch', this.fetch.bind(this)) - scope.addEventListener( 'notificationclick', this.notification_click.bind(this)) + $mol_wire_sync(this).plugins_add_wait() + this.plugins() + return scope + } + + static async plugins_add_wait() { + await Promise.resolve() + } - this.plugins = Object.values(this.$.$mol_service) + @ $mol_mem + static plugins() { + return Object.values(this.$.$mol_service) as ( + typeof $mol_service_plugin + | typeof $mol_service_plugin_notify + | typeof $mol_service_plugin_cache + )[] } - protected static log_error(plugin: typeof $mol_service_plugin, error: unknown) { - ;(error as Error).message = `${plugin.toString()}: ${(error as Error).message}` - console.error(error) + protected static log_plugin_error(plugin: typeof $mol_service_plugin, error: unknown) { + if ($mol_fail_catch(error)) { + ;(error as Error).message = `${plugin.toString()}: ${(error as Error).message}` + console.error(error) + } } + @ $mol_mem protected static scope() { - return self as unknown as ServiceWorkerGlobalScope + const scope = self as unknown as ServiceWorkerGlobalScope + + scope.addEventListener( 'install' , event => { + scope.skipWaiting() + event.waitUntil($mol_wire_async(this).install(event)) + }) + + scope.addEventListener( 'activate' , event => { + event.waitUntil($mol_wire_async(this).activate(event)) + }) + + scope.addEventListener( 'message', event => { + event.waitUntil( $mol_wire_async(this).message( + $mol_service_message_event_web.make({ event }) + ) ) + }) + + scope.addEventListener( 'fetch', event => { + event.waitUntil($mol_wire_async(this).fetch(event)) + }) + + scope.addEventListener( 'notificationclick', event => { + event.waitUntil($mol_wire_async(this).notification_click(event)) + }) + + return scope } - static clients() { - return this.scope().clients + protected static clients() { + return $mol_wire_sync(this.scope().clients) } - protected static send_delayed = [] as {}[] + static claim() { return this.clients().claim() } + + @ $mol_mem_key + static client(id: string) { + const client = this.clients().get(id) + return client ? $mol_wire_sync(client) : null + } @ $mol_action - static override send(data: {}) { - const worker = this.in_worker() ? this.worker() : null + static clients_filter( + query?: Query + ) { + return this.clients().matchAll(query) + .map(client => $mol_wire_sync(client)) as unknown as readonly ( + Query['type'] extends 'window' + ? WindowClient + : Client + )[] + } - if (worker) { - try { - worker.postMessage(data) - } catch (error) { - console.error(error) - } + static window_open(url: string | URL) { + return this.clients().openWindow(url) + } - return + @ $mol_action + static override send(data: {}) { + if (this.in_worker()) { + throw new Error('Worker can\'t send messages, use clients api') } - this.send_delayed.push(data) - - if (this.in_worker() && ! this.send_clients_promise) { - this.send_clients_promise = this.send_clients_async() - } + return $mol_wire_sync(this).send_async(data) } - protected static send_clients_promise = null as null | Promise + @ $mol_action + static send_async(data: Record) { + const active = this.registration_ready().active! - protected static async send_clients_async() { + // https://github.com/GoogleChrome/samples/blob/gh-pages/service-worker/post-message/demo.js + return new Promise((reject, resolve) => { + const channel = new MessageChannel() - let clients = [] as readonly WindowClient[] + const handler = setTimeout( + () => reject(new Error('Service worker not responding', { cause: channel })), + this.send_timeout() + ) - try { - clients = await this.clients().matchAll({ includeUncontrolled: true, type: 'window' }) - } catch (error) { - console.error(error) - } + channel.port1.onmessage = event => { + clearTimeout(handler) + const data = event.data + const message = data?.error ?? (data?.result ? null : 'empty data') + if (message) return reject(new Error(message, { cause: event })) - for (const client of clients) { - for (const data of this.send_delayed) { - try { - client.postMessage(data) - } catch (error) { - console.error(error) - } + resolve(event.data.result) } - } - - this.send_delayed = [] - this.send_clients_promise = null + channel.port1.onmessageerror = event => { + clearTimeout(handler) + reject(new Error('Can\'t be deserialized: ' + event.data, { cause: event })) + } - return null + active.postMessage(data, [ channel.port2 ]) + }) } - static message(event: ExtendableMessageEvent) { - const data = event.data as string | null | { - [k: string]: unknown - } - if ( ! data || typeof data !== 'object' ) return false + static message(event: $mol_service_message_event) { + const data = event.data() + if (! data) return - for (const plugin of this.plugins) { + for (const plugin of this.plugins()) { try { const result = plugin.message_data(data) - if (result) event.waitUntil(result) + if (result) return event.result(result) } catch (error) { - this.log_error(plugin, error) + event.error(error as Error) + this.log_plugin_error(plugin, error) + return null } } + event.result(null) + return null } static install(event: ExtendableEvent) { - for (const plugin of this.plugins) { + for (const plugin of this.plugins()) { try { const result = plugin.install() - if (result) event.waitUntil(result) + if (result) return result } catch (error) { - this.log_error(plugin, error) + this.log_plugin_error(plugin, error) } } - - this.scope().skipWaiting() } static activate(event: ExtendableEvent) { - for (const plugin of this.plugins) { + for (const plugin of this.plugins()) { try { const result = plugin.activate() - if (result) event.waitUntil(result) + if (result) return result } catch (error) { - this.log_error(plugin, error) + this.log_plugin_error(plugin, error) } } @@ -211,44 +244,46 @@ namespace $ { } static notification_click(event: NotificationEvent) { - for (const plugin of this.plugins) { + for (const plugin of this.plugins()) { if ( ! ( plugin.prototype instanceof $mol_service_plugin_notify ) ) continue try { const result = (plugin as typeof $mol_service_plugin_notify).notification(event.notification) - if ( result ) return event.waitUntil(result) + if ( result ) return result } catch (error) { - this.log_error(plugin, error) + this.log_plugin_error(plugin, error) } } } - static fetch(event: FetchEvent) { - const request = event.request - - for (const plugin of this.plugins) { + @ $mol_action + static block(event: FetchEvent) { + for (const plugin of this.plugins()) { if ( ! ( plugin.prototype instanceof $mol_service_plugin_cache ) ) continue try { - if ((plugin as typeof $mol_service_plugin_cache).blocked(request)) { + if ((plugin as typeof $mol_service_plugin_cache).blocked(event.request)) { return event.respondWith(this.blocked_response()) } } catch (error) { - this.log_error(plugin, error) + this.log_plugin_error(plugin, error) } } + } + static fetch(event: FetchEvent) { + this.block(event) const waitUntil = event.waitUntil.bind(event) - for (const plugin of this.plugins) { + for (const plugin of this.plugins()) { if ( ! ( plugin.prototype instanceof $mol_service_plugin_cache ) ) continue try { - const response = (plugin as typeof $mol_service_plugin_cache).modify(request, waitUntil) + const response = (plugin as typeof $mol_service_plugin_cache).modify(event.request, waitUntil) if (response) return event.respondWith(response) } catch (error) { - this.log_error(plugin, error) + this.log_plugin_error(plugin, error) } } } From 00dcb498df3999a61f6749552ea43bf930ccad19 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 2 Nov 2024 11:00:42 +0300 Subject: [PATCH 41/60] $mol_service refactor --- service/worker/worker.web.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index 39d81f8ab58..6eaaae3705d 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -9,11 +9,12 @@ namespace $ { static init() { $mol_wire_solid() try { - if ( this.in_worker() ) this.scope_inited() - else this.registration_inited() + if ( this.in_worker() ) this.scope_ready() + else this.registration_ready() } catch (error) { this.$.$mol_fail_log(error) } + return null } @ $mol_mem @@ -34,13 +35,12 @@ namespace $ { @ $mol_mem static registration() { - $mol_wire_solid() const reg = this.in_worker() ? this.scope().registration : this.container().register( this.path() ) return $mol_wire_sync(reg) } @ $mol_mem - static registration_inited() { + static registration_events_attached() { const reg = this.registration() if (reg.waiting) this.state(null) else if (reg.installing) this.update_found() @@ -54,8 +54,7 @@ namespace $ { @ $mol_mem static registration_ready() { - $mol_wire_solid() - this.registration_inited() + this.registration_events_attached() return $mol_wire_sync(this).registration_ready_async() } @@ -73,7 +72,7 @@ namespace $ { static state(reset?: null) { return this.worker()?.state ?? null } @ $mol_mem - static scope_inited() { + static scope_ready() { const scope = this.scope() $mol_wire_sync(this).plugins_add_wait() this.plugins() From 33b1fc69260d68f88932e8282c734396e50eb7d9 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 2 Nov 2024 14:11:03 +0300 Subject: [PATCH 42/60] $mol_service refactor --- notify/notify.ts | 10 ++- notify/notify.web.ts | 11 +-- offline/offline.ts | 4 + offline/offline.web.ts | 8 +- service/channel/channel.ts | 41 +++++++++++ service/plugin/plugin.ts | 18 ++--- service/service.ts | 32 +++++++- .../{worker/worker.web.ts => service.web.ts} | 73 +++++++------------ service/worker/worker.ts | 31 -------- 9 files changed, 124 insertions(+), 104 deletions(-) create mode 100644 service/channel/channel.ts rename service/{worker/worker.web.ts => service.web.ts} (78%) delete mode 100644 service/worker/worker.ts diff --git a/notify/notify.ts b/notify/notify.ts index 59d652bceda..48a1514eacf 100644 --- a/notify/notify.ts +++ b/notify/notify.ts @@ -14,13 +14,13 @@ namespace $ { } static show( info: $mol_notify_info ) { - this.$.$mol_service_worker.send(info) + this.$.$mol_service.send(info) } } export class $mol_notify_service extends $mol_service_plugin_notify { - static override message_data(data: {}) { + static override data(data: {}) { if ('uri' in data && 'message' in data) { this.show(data as $mol_notify_info) return true @@ -31,5 +31,9 @@ namespace $ { static show(info: $mol_notify_info) {} } - + + export namespace $mol_service_plugin { + export let $mol_notify_service = $.$mol_notify_service + } + } diff --git a/notify/notify.web.ts b/notify/notify.web.ts index 30abdc78345..a8498d6882b 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -31,7 +31,7 @@ namespace $ { export class $mol_notify_service_web extends $mol_notify_service { static override show({ context: title, message: body, uri: data }: $mol_notify_info) { - const registration = this.$.$mol_service_worker_web.registration() + const registration = this.$.$mol_service_web.registration() const tag = data const existen = registration.getNotifications({ tag }) @@ -50,7 +50,7 @@ namespace $ { } static override notification( notification: Notification ) { - const matched = this.$.$mol_service_worker_web.clients_filter({ includeUncontrolled: true, type: 'window' }) + const matched = this.$.$mol_service_web.clients_filter({ includeUncontrolled: true, type: 'window' }) const last = matched.at(-1) if( last ) { @@ -60,15 +60,12 @@ namespace $ { return null } - this.$.$mol_service_worker_web.window_open( notification.data ) + this.$.$mol_service_web.window_open( notification.data ) return null } } $.$mol_notify_service = $mol_notify_service_web - export namespace $mol_service { - export const $mol_notify_service = $mol_notify_service_web - } - + $mol_service_plugin.$mol_notify_service = $mol_notify_service_web } diff --git a/offline/offline.ts b/offline/offline.ts index 6e4fff3dd29..70cdf58d65a 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -70,4 +70,8 @@ namespace $ { return clone } } + + export namespace $mol_service_plugin { + export let $mol_offline = $.$mol_offline + } } diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 0127e621371..69336a681b4 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -9,14 +9,10 @@ namespace $ { } static override activate() { - return this.$.$mol_service_worker_web.claim() + return this.$.$mol_service_web.claim() } } $.$mol_offline = $mol_offline_web - - export namespace $mol_service { - export const $mol_offline = $mol_offline_web - } - + $mol_service_plugin.$mol_offline = $mol_offline_web } diff --git a/service/channel/channel.ts b/service/channel/channel.ts new file mode 100644 index 00000000000..5ba44d97a4c --- /dev/null +++ b/service/channel/channel.ts @@ -0,0 +1,41 @@ +namespace $ { + export class $mol_service_channel extends $mol_object { + protected channel = new MessageChannel() + + timeout() { return 20000 } + + out() { return this.channel.port2 } + + result() { return $mol_wire_sync(this).result_async() } + + result_async() { + return new Promise((resolve, reject) => { + const channel = this.channel + + const handler = setTimeout( + () => reject(new Error('Channel timeout', { cause: channel })), + this.timeout() + ) + + this.destructor = () => { + clearTimeout(handler) + reject(new Error('Channel cancelled')) + } + + channel.port1.onmessage = event => { + clearTimeout(handler) + const data = event.data + const message = data?.error ?? (data?.result ? null : 'empty data') + if (message) return reject(new Error(message, { cause: event })) + + resolve(event.data.result) + } + + channel.port1.onmessageerror = event => { + clearTimeout(handler) + reject(new Error('Can\'t be deserialized: ' + event.data, { cause: event })) + } + }) + } + } +} diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 55357c7dd5a..952a9bd2b5f 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -1,20 +1,20 @@ namespace $ { - export class $mol_service_plugin extends $mol_object { + export namespace $mol_service_plugin { + let _ + } + + export class $mol_service_plugin_base extends $mol_object { static install() { return null as unknown } static activate() { return null as unknown } - static message_data(data: {}) { return null as unknown } - - static service() { return this.$.$mol_service_worker } + static data(data: {}) { return null as unknown } } - export class $mol_service_plugin_cache extends $mol_service_plugin { + export class $mol_service_plugin_cache extends $mol_service_plugin_base { static blocked(request: Request) { return false } - static modify(request: Request, waitUntil: (promise: Promise) => void) { - return null as null | Response - } + static modify(request: Request) { return null as null | Response } } - export class $mol_service_plugin_notify extends $mol_service_plugin { + export class $mol_service_plugin_notify extends $mol_service_plugin_base { static notification(e: unknown) { return null as unknown } } } diff --git a/service/service.ts b/service/service.ts index e7517e92b48..3e77c960e40 100644 --- a/service/service.ts +++ b/service/service.ts @@ -1,3 +1,31 @@ -namespace $.$mol_service { - let _ +namespace $ { + export class $mol_service extends $mol_object { + static path() { return 'web.js' } + + static send_timeout() { return 20000 } + + @ $mol_action + static send(data: {}) { return null as unknown } + + @ $mol_mem + static prepare(next?: $mol_service_prepare_event) { + return next ? next : null + } + + @ $mol_mem + static prepare_choise() { return $mol_wire_sync(this).choise_promise()?.outcome ?? null } + + static choise_promise() { return this.prepare()?.userChoise } + + static blocked_response() { + return new Response( + null, + { + status: 418, + statusText: 'Blocked' + }, + ) + } + } + } diff --git a/service/worker/worker.web.ts b/service/service.web.ts similarity index 78% rename from service/worker/worker.web.ts rename to service/service.web.ts index 6eaaae3705d..1627ddb576e 100644 --- a/service/worker/worker.web.ts +++ b/service/service.web.ts @@ -2,7 +2,7 @@ namespace $ { - export class $mol_service_worker_web extends $mol_service_worker { + export class $mol_service_web extends $mol_service { protected static in_worker() { return typeof window === 'undefined' } @ $mol_mem @@ -40,7 +40,7 @@ namespace $ { } @ $mol_mem - static registration_events_attached() { + protected static registration_events_attached() { const reg = this.registration() if (reg.waiting) this.state(null) else if (reg.installing) this.update_found() @@ -85,14 +85,14 @@ namespace $ { @ $mol_mem static plugins() { - return Object.values(this.$.$mol_service) as ( - typeof $mol_service_plugin + return Object.values(this.$.$mol_service_plugin) as ( + typeof $mol_service_plugin_base | typeof $mol_service_plugin_notify | typeof $mol_service_plugin_cache )[] } - protected static log_plugin_error(plugin: typeof $mol_service_plugin, error: unknown) { + protected static log_plugin_error(plugin: typeof $mol_service_plugin_base, error: unknown) { if ($mol_fail_catch(error)) { ;(error as Error).message = `${plugin.toString()}: ${(error as Error).message}` console.error(error) @@ -104,6 +104,7 @@ namespace $ { const scope = self as unknown as ServiceWorkerGlobalScope scope.addEventListener( 'install' , event => { + // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting#examples scope.skipWaiting() event.waitUntil($mol_wire_async(this).install(event)) }) @@ -158,43 +159,22 @@ namespace $ { } @ $mol_action + protected static post_message(data: unknown) { + const channel = this.$.$mol_service_channel.make({ + timeout: () => this.send_timeout() + }) + + this.registration_ready().active!.postMessage(data, [ channel.out() ]) + + return channel + } + static override send(data: {}) { if (this.in_worker()) { throw new Error('Worker can\'t send messages, use clients api') } - return $mol_wire_sync(this).send_async(data) - } - - @ $mol_action - static send_async(data: Record) { - const active = this.registration_ready().active! - - // https://github.com/GoogleChrome/samples/blob/gh-pages/service-worker/post-message/demo.js - return new Promise((reject, resolve) => { - const channel = new MessageChannel() - - const handler = setTimeout( - () => reject(new Error('Service worker not responding', { cause: channel })), - this.send_timeout() - ) - - channel.port1.onmessage = event => { - clearTimeout(handler) - const data = event.data - const message = data?.error ?? (data?.result ? null : 'empty data') - if (message) return reject(new Error(message, { cause: event })) - - resolve(event.data.result) - } - - channel.port1.onmessageerror = event => { - clearTimeout(handler) - reject(new Error('Can\'t be deserialized: ' + event.data, { cause: event })) - } - - active.postMessage(data, [ channel.port2 ]) - }) + return this.post_message(data).result() } static message(event: $mol_service_message_event) { @@ -203,7 +183,7 @@ namespace $ { for (const plugin of this.plugins()) { try { - const result = plugin.message_data(data) + const result = plugin.data(data) if (result) return event.result(result) } catch (error) { event.error(error as Error) @@ -257,29 +237,30 @@ namespace $ { } @ $mol_action - static block(event: FetchEvent) { + static block(request: Request) { for (const plugin of this.plugins()) { if ( ! ( plugin.prototype instanceof $mol_service_plugin_cache ) ) continue try { - if ((plugin as typeof $mol_service_plugin_cache).blocked(event.request)) { - return event.respondWith(this.blocked_response()) + if ((plugin as typeof $mol_service_plugin_cache).blocked(request)) { + return this.blocked_response() } } catch (error) { this.log_plugin_error(plugin, error) } } + return null } static fetch(event: FetchEvent) { - this.block(event) - const waitUntil = event.waitUntil.bind(event) + const response = this.block(event.request) + if (response) return event.respondWith(response) for (const plugin of this.plugins()) { if ( ! ( plugin.prototype instanceof $mol_service_plugin_cache ) ) continue try { - const response = (plugin as typeof $mol_service_plugin_cache).modify(event.request, waitUntil) + const response = (plugin as typeof $mol_service_plugin_cache).modify(event.request) if (response) return event.respondWith(response) } catch (error) { this.log_plugin_error(plugin, error) @@ -288,8 +269,8 @@ namespace $ { } } - $.$mol_service_worker = $mol_service_worker_web + $.$mol_service = $mol_service_web - $mol_service_worker_web.init() + $mol_service_web.init() } diff --git a/service/worker/worker.ts b/service/worker/worker.ts deleted file mode 100644 index 5d99b35c6f5..00000000000 --- a/service/worker/worker.ts +++ /dev/null @@ -1,31 +0,0 @@ -namespace $ { - export class $mol_service_worker extends $mol_object { - static path() { return 'web.js' } - - static send_timeout() { return 20000 } - - @ $mol_action - static send(data: {}) { return null as unknown } - - @ $mol_mem - static prepare(next?: $mol_service_prepare_event) { - return next ? next : null - } - - @ $mol_mem - static prepare_choise() { return $mol_wire_sync(this).choise_promise()?.outcome ?? null } - - static choise_promise() { return this.prepare()?.userChoise } - - static blocked_response() { - return new Response( - null, - { - status: 418, - statusText: 'Blocked' - }, - ) - } - } - -} From 3ee7e45adf44b5b9a4f44e682e6f196ac39a3010 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sat, 2 Nov 2024 22:34:56 +0300 Subject: [PATCH 43/60] $mol_worker async --- fail/catch/catch.ts | 2 +- fetch/fetch.ts | 5 + notify/notify.ts | 2 +- notify/notify.web.ts | 6 +- offline/install/install.ts | 2 +- offline/offline.ts | 55 +++--- offline/offline.web.ts | 18 -- service/channel/channel.ts | 18 +- service/message/event/event.ts | 4 +- service/message/event/event.web.ts | 9 +- service/plugin/plugin.ts | 26 ++- service/{service.ts => worker/worker.ts} | 6 +- .../{service.web.ts => worker/worker.web.ts} | 175 +++++++++++------- 13 files changed, 187 insertions(+), 141 deletions(-) delete mode 100644 offline/offline.web.ts rename service/{service.ts => worker/worker.ts} (85%) rename service/{service.web.ts => worker/worker.web.ts} (60%) diff --git a/fail/catch/catch.ts b/fail/catch/catch.ts index 428a97b7b0f..b8ed371654d 100644 --- a/fail/catch/catch.ts +++ b/fail/catch/catch.ts @@ -2,7 +2,7 @@ namespace $ { const catched = new WeakMap< any , boolean >() - export function $mol_fail_catch( error: unknown ) { + export function $mol_fail_catch( error: unknown ): error is Error { if( typeof error !== 'object' ) return false if( $mol_promise_like( error ) ) $mol_fail_hidden( error ) diff --git a/fetch/fetch.ts b/fetch/fetch.ts index 126cf87afba..76cdcc7c4cb 100644 --- a/fetch/fetch.ts +++ b/fetch/fetch.ts @@ -6,6 +6,11 @@ namespace $ { super() } + @ $mol_action + clone() { + return new this.$.$mol_fetch_response(new Response(this.native.body, this.native)) + } + status() { const types = [ 'unknown', 'inform', 'success', 'redirect', 'wrong', 'failed' ] as const return types[ Math.floor( this.native.status / 100 ) ] diff --git a/notify/notify.ts b/notify/notify.ts index 48a1514eacf..99999e96340 100644 --- a/notify/notify.ts +++ b/notify/notify.ts @@ -14,7 +14,7 @@ namespace $ { } static show( info: $mol_notify_info ) { - this.$.$mol_service.send(info) + this.$.$mol_service_worker.send(info) } } diff --git a/notify/notify.web.ts b/notify/notify.web.ts index a8498d6882b..7c45ea3ec26 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -31,7 +31,7 @@ namespace $ { export class $mol_notify_service_web extends $mol_notify_service { static override show({ context: title, message: body, uri: data }: $mol_notify_info) { - const registration = this.$.$mol_service_web.registration() + const registration = this.$.$mol_service_worker_web.registration() const tag = data const existen = registration.getNotifications({ tag }) @@ -50,7 +50,7 @@ namespace $ { } static override notification( notification: Notification ) { - const matched = this.$.$mol_service_web.clients_filter({ includeUncontrolled: true, type: 'window' }) + const matched = this.$.$mol_service_worker_web.clients_filter({ includeUncontrolled: true, type: 'window' }) const last = matched.at(-1) if( last ) { @@ -60,7 +60,7 @@ namespace $ { return null } - this.$.$mol_service_web.window_open( notification.data ) + this.$.$mol_service_worker_web.window_open( notification.data ) return null } } diff --git a/offline/install/install.ts b/offline/install/install.ts index 79e40bb7835..2cc8af51adb 100644 --- a/offline/install/install.ts +++ b/offline/install/install.ts @@ -1,3 +1,3 @@ namespace $ { - $.$mol_offline + $mol_offline } diff --git a/offline/offline.ts b/offline/offline.ts index 70cdf58d65a..cc01c326131 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -11,63 +11,56 @@ namespace $ { return this.blocked_urls.includes(normalized_url) } - static 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) + static override activate() { + return this.$.$mol_service_worker.claim() } - protected static fetch(request: Request) { - return null as null | Response + static override need_modify(request: Request) { + if( request.method !== 'GET' ) return false + if( !/^https?:/.test( request.url ) ) return false + if( /\?/.test( request.url ) ) return false + if( request.cache === 'no-store' ) return false + + return true } - protected static respond(request: Request) { + static override modify(request: Request) { let fallback_header - const url = new URL(request.url) - const html = url.pathname.endsWith('.html') + const html = request.mode === 'navigate' const cache = request.cache if (cache === 'reload' || ( cache === 'no-cache' && ! html ) ) { if (cache === 'reload') { // F5 + Disable cache - request = new Request(request, { cache: 'no-cache' }) + request = this.request_clone(request, { cache: 'no-cache' }) } // fetch with fallback to cache if statuses not match try { - const actual = this.fetch(request) - if (! actual) return null - if (actual.status < 400) return actual - - throw new Error( - `${actual.status}${actual.statusText ? ` ${actual.statusText}` : ''}`, - { cause: actual } - ) + const actual = this.$.$mol_fetch.response(request) + if (actual.code() < 400) return actual.native + fallback_header = actual.message() } catch (err) { - fallback_header = `${(err as Error).cause instanceof Response ? '' : '500 '}${ - (err as Error).message} $mol_offline fallback to cache` + if ( $mol_promise_like(err) ) $mol_fail_hidden(err) + fallback_header = (err as Error).message || 'Fetch error' } } if (cache !== 'force-cache') { - request = new Request(request, { cache: 'force-cache' }) + request = this.request_clone(request, { cache: 'force-cache' }) } - const cached = this.fetch(request) - if (! cached) return null - if (! fallback_header || cached.headers.get('$mol_offline_remote_status')) return cached + const cached = this.$.$mol_fetch.response(request) + + if (! fallback_header ) return cached.native - const clone = new Response(cached.body, cached) - clone.headers.set( '$mol_offline_remote_status', fallback_header ?? '') + const clone = cached.clone() + clone.headers().set( '$mol_offline_remote_status', `${fallback_header} $mol_offline fallback to cache`) - return clone + return clone.native } } diff --git a/offline/offline.web.ts b/offline/offline.web.ts deleted file mode 100644 index 69336a681b4..00000000000 --- a/offline/offline.web.ts +++ /dev/null @@ -1,18 +0,0 @@ -namespace $ { - export class $mol_offline_web extends $mol_offline { - protected static override fetch(request: Request) { - return $mol_wire_sync(this).fetch_async(request) - } - - static fetch_async(request: Request) { - return fetch(request) - } - - static override activate() { - return this.$.$mol_service_web.claim() - } - } - - $.$mol_offline = $mol_offline_web - $mol_service_plugin.$mol_offline = $mol_offline_web -} diff --git a/service/channel/channel.ts b/service/channel/channel.ts index 5ba44d97a4c..985b03db925 100644 --- a/service/channel/channel.ts +++ b/service/channel/channel.ts @@ -25,15 +25,23 @@ namespace $ { channel.port1.onmessage = event => { clearTimeout(handler) const data = event.data - const message = data?.error ?? (data?.result ? null : 'empty data') - if (message) return reject(new Error(message, { cause: event })) - - resolve(event.data.result) + const result = data?.result + const error = data?.error + + if (result) { + if (error) console.warn('Message result+error:', error) + resolve(result) + return + } + + if (! error) return resolve(result ?? null) + + reject(new Error(error, { cause: event })) } channel.port1.onmessageerror = event => { clearTimeout(handler) - reject(new Error('Can\'t be deserialized: ' + event.data, { cause: event })) + reject(new Error('Message fatal error: ' + event.data, { cause: event })) } }) } diff --git a/service/message/event/event.ts b/service/message/event/event.ts index 0f58e7df913..e9fd896aed5 100644 --- a/service/message/event/event.ts +++ b/service/message/event/event.ts @@ -4,8 +4,6 @@ namespace $ { return null as null | Record } - result(result: {} | null) {} - - error(error: Error) {} + result(result: {} | null, errors?: readonly Error[]) {} } } diff --git a/service/message/event/event.web.ts b/service/message/event/event.web.ts index dd2d9510bdf..93e4aa0997b 100644 --- a/service/message/event/event.web.ts +++ b/service/message/event/event.web.ts @@ -11,14 +11,11 @@ namespace $ { } @ $mol_action - override result(result: {} | null) { - this.event.ports[0].postMessage({ error: null, result }) + override result(result: {} | null, errors?: readonly Error[]) { + const error = errors?.length ? errors[0].toString() || errors[0].message : null + this.event.ports[0].postMessage({ error, result }) } - @ $mol_action - override error(error: Error) { - this.event.ports[0].postMessage({ error: error.toString(), result: null }) - } } $.$mol_service_message_event = $mol_service_message_event_web diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 952a9bd2b5f..70bb4a9a0fc 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -1,20 +1,36 @@ namespace $ { + $mol_service_worker.init() + export namespace $mol_service_plugin { let _ } export class $mol_service_plugin_base extends $mol_object { - static install() { return null as unknown } - static activate() { return null as unknown } - static data(data: {}) { return null as unknown } + + static is( + this: This, + some: { prototype: unknown } + ): some is This { + return some.prototype instanceof this + } + + static install() { } + static activate() { } + static data(data: {}) { return null as null | unknown } } export class $mol_service_plugin_cache extends $mol_service_plugin_base { static blocked(request: Request) { return false } - static modify(request: Request) { return null as null | Response } + static need_modify(request: Request) { return false } + static modify(request: Request) { return new Response } + @ $mol_action + static request_clone(original: Request, options?: RequestInit) { + return new Request(original, options) + } } export class $mol_service_plugin_notify extends $mol_service_plugin_base { - static notification(e: unknown) { return null as unknown } + static notification(e: unknown) { } } + } diff --git a/service/service.ts b/service/worker/worker.ts similarity index 85% rename from service/service.ts rename to service/worker/worker.ts index 3e77c960e40..7b40fde4d9b 100644 --- a/service/service.ts +++ b/service/worker/worker.ts @@ -1,5 +1,5 @@ namespace $ { - export class $mol_service extends $mol_object { + export class $mol_service_worker extends $mol_object { static path() { return 'web.js' } static send_timeout() { return 20000 } @@ -7,11 +7,15 @@ namespace $ { @ $mol_action static send(data: {}) { return null as unknown } + static init() {} + @ $mol_mem static prepare(next?: $mol_service_prepare_event) { return next ? next : null } + static claim() {} + @ $mol_mem static prepare_choise() { return $mol_wire_sync(this).choise_promise()?.outcome ?? null } diff --git a/service/service.web.ts b/service/worker/worker.web.ts similarity index 60% rename from service/service.web.ts rename to service/worker/worker.web.ts index 1627ddb576e..90f54b513f7 100644 --- a/service/service.web.ts +++ b/service/worker/worker.web.ts @@ -2,7 +2,7 @@ namespace $ { - export class $mol_service_web extends $mol_service { + export class $mol_service_worker_web extends $mol_service_worker { protected static in_worker() { return typeof window === 'undefined' } @ $mol_mem @@ -39,30 +39,40 @@ namespace $ { return $mol_wire_sync(reg) } + @ $mol_mem_key + protected static registration_direct(key: 'active' | 'installing' | 'waiting') { + this.state() + return this.registration()[key] ?? null + } + @ $mol_mem - protected static registration_events_attached() { - const reg = this.registration() - if (reg.waiting) this.state(null) - else if (reg.installing) this.update_found() - else { - reg.addEventListener( 'updatefound', this.update_found.bind(this)) - } - return reg + protected static registration_event_installing() { + const reg = this.registration_direct('installing') + reg?.addEventListener( 'statechange', e => this.state(null)) + return null + } + + @ $mol_mem + protected static registration_event_active() { + const reg = this.registration_direct('active') + reg?.addEventListener( 'updatefound', e => { + this.worker()!.addEventListener( 'statechange', e => this.state(null)) + }) + + return null } static registration_ready_async() { return this.container().ready } @ $mol_mem static registration_ready() { - this.registration_events_attached() + this.state() + if (this.registration_direct('waiting')) this.state(null) + this.registration_event_installing() + this.registration_event_active() return $mol_wire_sync(this).registration_ready_async() } - protected static update_found() { - const worker = this.registration().installing! - worker.addEventListener( 'statechange', e => this.state(null)) - } - protected static worker() { const reg = this.registration() return reg.installing ?? reg.waiting ?? reg.active @@ -83,22 +93,6 @@ namespace $ { await Promise.resolve() } - @ $mol_mem - static plugins() { - return Object.values(this.$.$mol_service_plugin) as ( - typeof $mol_service_plugin_base - | typeof $mol_service_plugin_notify - | typeof $mol_service_plugin_cache - )[] - } - - protected static log_plugin_error(plugin: typeof $mol_service_plugin_base, error: unknown) { - if ($mol_fail_catch(error)) { - ;(error as Error).message = `${plugin.toString()}: ${(error as Error).message}` - console.error(error) - } - } - @ $mol_mem protected static scope() { const scope = self as unknown as ServiceWorkerGlobalScope @@ -120,7 +114,8 @@ namespace $ { }) scope.addEventListener( 'fetch', event => { - event.waitUntil($mol_wire_async(this).fetch(event)) + const response = this.fetch(event) + if (response) event.respondWith(response) }) scope.addEventListener( 'notificationclick', event => { @@ -134,7 +129,7 @@ namespace $ { return $mol_wire_sync(this.scope().clients) } - static claim() { return this.clients().claim() } + static override claim() { return this.clients().claim() } @ $mol_mem_key static client(id: string) { @@ -181,27 +176,42 @@ namespace $ { const data = event.data() if (! data) return + let errors = [] + let result + for (const plugin of this.plugins()) { try { const result = plugin.data(data) - if (result) return event.result(result) + if (result) break } catch (error) { - event.error(error as Error) - this.log_plugin_error(plugin, error) - return null + if ( $mol_fail_catch(error) ) { + this.$.$mol_log3_fail({ + place: `${plugin}.data()`, + message: error.message, + error, + }) + errors.push(error) + } } } - event.result(null) + + event.result(result ?? null, errors) + return null } static install(event: ExtendableEvent) { for (const plugin of this.plugins()) { try { - const result = plugin.install() - if (result) return result + plugin.install() } catch (error) { - this.log_plugin_error(plugin, error) + if ($mol_fail_catch(error)) { + this.$.$mol_log3_fail({ + place: `${plugin}.install()`, + message: error.message, + error, + }) + } } } } @@ -209,10 +219,15 @@ namespace $ { static activate(event: ExtendableEvent) { for (const plugin of this.plugins()) { try { - const result = plugin.activate() - if (result) return result + plugin.activate() } catch (error) { - this.log_plugin_error(plugin, error) + if ($mol_fail_catch(error)) { + this.$.$mol_log3_fail({ + place: `${plugin}.activate()`, + message: error.message, + error, + }) + } } } @@ -222,31 +237,54 @@ namespace $ { }) } - static notification_click(event: NotificationEvent) { - for (const plugin of this.plugins()) { - if ( ! ( plugin.prototype instanceof $mol_service_plugin_notify ) ) continue + @ $mol_mem + static plugins_raw() { + return Object.values(this.$.$mol_service_plugin) as readonly { prototype: unknown }[] + } + static plugins() { + return this.plugins_raw().filter(plugin => $mol_service_plugin_base.is(plugin)) + } + + static plugins_notify() { + return this.plugins_raw().filter(plugin => $mol_service_plugin_notify.is(plugin)) + } + + static plugins_cache() { + return this.plugins_raw().filter(plugin => $mol_service_plugin_cache.is(plugin)) + } + + static notification_click(event: NotificationEvent) { + for (const plugin of this.plugins_notify()) { try { - const result = (plugin as typeof $mol_service_plugin_notify).notification(event.notification) - if ( result ) return result + plugin.notification(event.notification) } catch (error) { - this.log_plugin_error(plugin, error) + if ($mol_fail_catch(error)) { + this.$.$mol_log3_fail({ + place: `${plugin}.notification()`, + message: error.message, + error, + }) + } } } - } @ $mol_action static block(request: Request) { - for (const plugin of this.plugins()) { - if ( ! ( plugin.prototype instanceof $mol_service_plugin_cache ) ) continue - + for (const plugin of this.plugins_cache()) { try { - if ((plugin as typeof $mol_service_plugin_cache).blocked(request)) { + if (plugin.blocked(request)) { return this.blocked_response() } } catch (error) { - this.log_plugin_error(plugin, error) + if ($mol_fail_catch(error)) { + this.$.$mol_log3_fail({ + place: `${plugin}.blocked()`, + message: error.message, + error, + }) + } } } return null @@ -254,23 +292,28 @@ namespace $ { static fetch(event: FetchEvent) { const response = this.block(event.request) - if (response) return event.respondWith(response) - - for (const plugin of this.plugins()) { - if ( ! ( plugin.prototype instanceof $mol_service_plugin_cache ) ) continue + if (response) return response + for (const plugin of this.plugins_cache()) { try { - const response = (plugin as typeof $mol_service_plugin_cache).modify(event.request) - if (response) return event.respondWith(response) + if (plugin.need_modify(event.request)) { + return $mol_wire_async(plugin).modify(event.request) + } } catch (error) { - this.log_plugin_error(plugin, error) + if ($mol_fail_catch(error)) { + this.$.$mol_log3_fail({ + place: `${plugin}.modify()`, + message: error.message, + error, + }) + } } } + + return null } } - $.$mol_service = $mol_service_web - - $mol_service_web.init() + $.$mol_service_worker = $mol_service_worker_web } From 6fabdacb19f0a3e60845f8000e44550891432064 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 3 Nov 2024 12:36:48 +0300 Subject: [PATCH 44/60] $mol_offline refactoring --- fetch/fetch.ts | 4 + notify/notify.web.ts | 2 +- service/ensure/ensure.ts | 5 ++ service/plugin/plugin.ts | 2 +- service/registration/registration.web.ts | 43 +++++++++ service/worker/worker.ts | 5 +- service/worker/worker.web.ts | 108 +++++++---------------- wire/sync/sync.ts | 17 +++- 8 files changed, 104 insertions(+), 82 deletions(-) create mode 100644 service/ensure/ensure.ts create mode 100644 service/registration/registration.web.ts diff --git a/fetch/fetch.ts b/fetch/fetch.ts index 76cdcc7c4cb..89734037ddc 100644 --- a/fetch/fetch.ts +++ b/fetch/fetch.ts @@ -80,6 +80,10 @@ namespace $ { } } + export function $mol_fetch_request_clone(this: typeof $, original: Request, options?: RequestInit) { + return new Request(original, options) + } + $.$mol_fetch_request_clone = $mol_wire_sync($).$mol_fetch_request_clone export class $mol_fetch extends $mol_object2 { diff --git a/notify/notify.web.ts b/notify/notify.web.ts index 7c45ea3ec26..5e9eff39c33 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -31,7 +31,7 @@ namespace $ { export class $mol_notify_service_web extends $mol_notify_service { static override show({ context: title, message: body, uri: data }: $mol_notify_info) { - const registration = this.$.$mol_service_worker_web.registration() + const registration = this.$.$mol_service_worker_web.registration().native() const tag = data const existen = registration.getNotifications({ tag }) diff --git a/service/ensure/ensure.ts b/service/ensure/ensure.ts new file mode 100644 index 00000000000..78b46e0a8b4 --- /dev/null +++ b/service/ensure/ensure.ts @@ -0,0 +1,5 @@ +namespace $ { + export function $mol_service_ensure() { + return typeof window === 'undefined' + } +} diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 70bb4a9a0fc..4c665e00f3b 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -24,7 +24,7 @@ namespace $ { static need_modify(request: Request) { return false } static modify(request: Request) { return new Response } @ $mol_action - static request_clone(original: Request, options?: RequestInit) { + protected static request_clone(original: Request, options?: RequestInit) { return new Request(original, options) } } diff --git a/service/registration/registration.web.ts b/service/registration/registration.web.ts new file mode 100644 index 00000000000..c6e56dc00f0 --- /dev/null +++ b/service/registration/registration.web.ts @@ -0,0 +1,43 @@ +namespace $ { + export class $mol_service_registration_web extends $mol_object { + constructor ( protected raw: ServiceWorkerRegistration ) { super() } + + @ $mol_mem + native() { + const reg = this.raw + // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/updatefound_event + const worker_reset = () => this.worker(null) + reg.addEventListener( 'updatefound', worker_reset) + + return $mol_wire_sync(Object.assign(reg, { + destructor: () => { + reg.removeEventListener( 'updatefound', worker_reset) + } + })) + } + + preload() { return $mol_wire_sync(this.native().navigationPreload) } + pushes() { return $mol_wire_sync(this.native().pushManager) } + + @ $mol_mem + worker(reset?: null) { + const reg = this.native() + const worker = reg.installing ?? reg.waiting ?? reg.active ?? $mol_fail(new Error('No worker in registration')) + + const state_reset = () => this.state(null) + worker.addEventListener( 'statechange', state_reset) + + return $mol_wire_sync(Object.assign(worker, { + destructor: () => worker.removeEventListener('statechange', state_reset) + })) + } + + protected container() { + return this.$.$mol_dom_context.navigator?.serviceWorker ?? $mol_fail(new Error('Service worker not found')) + } + + @ $mol_mem + state(reset?: null) { return this.worker().state ?? null } + + } +} diff --git a/service/worker/worker.ts b/service/worker/worker.ts index 7b40fde4d9b..2bd7cb2f52f 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -3,15 +3,14 @@ namespace $ { static path() { return 'web.js' } static send_timeout() { return 20000 } - - @ $mol_action static send(data: {}) { return null as unknown } static init() {} @ $mol_mem static prepare(next?: $mol_service_prepare_event) { - return next ? next : null + next?.prompt() + return next ?? null } static claim() {} diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index 90f54b513f7..658ab50ee5d 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -3,14 +3,17 @@ namespace $ { export class $mol_service_worker_web extends $mol_service_worker { - protected static in_worker() { return typeof window === 'undefined' } + static plugins_add_wait() { return Promise.resolve() } @ $mol_mem static init() { $mol_wire_solid() try { - if ( this.in_worker() ) this.scope_ready() - else this.registration_ready() + if ( this.$.$mol_service_ensure() ) { + $mol_wire_sync(this).plugins_add_wait() + this.plugins() + } + this.registration().worker() } catch (error) { this.$.$mol_fail_log(error) } @@ -18,7 +21,7 @@ namespace $ { } @ $mol_mem - static container() { + protected static container() { const win = this.$.$mol_dom_context if( ! win.navigator.serviceWorker ) { @@ -35,63 +38,32 @@ namespace $ { @ $mol_mem static registration() { - const reg = this.in_worker() ? this.scope().registration : this.container().register( this.path() ) - return $mol_wire_sync(reg) - } - - @ $mol_mem_key - protected static registration_direct(key: 'active' | 'installing' | 'waiting') { - this.state() - return this.registration()[key] ?? null - } - - @ $mol_mem - protected static registration_event_installing() { - const reg = this.registration_direct('installing') - reg?.addEventListener( 'statechange', e => this.state(null)) - return null - } - - @ $mol_mem - protected static registration_event_active() { - const reg = this.registration_direct('active') - reg?.addEventListener( 'updatefound', e => { - this.worker()!.addEventListener( 'statechange', e => this.state(null)) - }) + const raw = this.$.$mol_service_ensure() + ? this.scope().registration + : this.container().register( this.path() ) - return null + return new this.$.$mol_service_registration_web(raw) } - static registration_ready_async() { return this.container().ready } + static ready_async() { return this.container().ready } + static ready() { return $mol_wire_sync( this ).ready_async() } - @ $mol_mem - static registration_ready() { - this.state() - if (this.registration_direct('waiting')) this.state(null) - this.registration_event_installing() - this.registration_event_active() - return $mol_wire_sync(this).registration_ready_async() - } + @ $mol_action + protected static post_message(data: unknown) { + if (this.$.$mol_service_ensure()) { + throw new Error('Worker can\'t send messages, use clients api') + } - protected static worker() { - const reg = this.registration() - return reg.installing ?? reg.waiting ?? reg.active - } + const channel = this.$.$mol_service_channel.make({ + timeout: () => this.send_timeout() + }) - @ $mol_mem - static state(reset?: null) { return this.worker()?.state ?? null } + this.ready().active?.postMessage(data, [ channel.out() ]) - @ $mol_mem - static scope_ready() { - const scope = this.scope() - $mol_wire_sync(this).plugins_add_wait() - this.plugins() - return scope + return channel } - static async plugins_add_wait() { - await Promise.resolve() - } + static send(data: {}) { return this.post_message(data).result() } @ $mol_mem protected static scope() { @@ -153,25 +125,6 @@ namespace $ { return this.clients().openWindow(url) } - @ $mol_action - protected static post_message(data: unknown) { - const channel = this.$.$mol_service_channel.make({ - timeout: () => this.send_timeout() - }) - - this.registration_ready().active!.postMessage(data, [ channel.out() ]) - - return channel - } - - static override send(data: {}) { - if (this.in_worker()) { - throw new Error('Worker can\'t send messages, use clients api') - } - - return this.post_message(data).result() - } - static message(event: $mol_service_message_event) { const data = event.data() if (! data) return @@ -300,13 +253,16 @@ namespace $ { return $mol_wire_async(plugin).modify(event.request) } } catch (error) { - if ($mol_fail_catch(error)) { - this.$.$mol_log3_fail({ + this.$.$mol_log3_fail($mol_promise_like(error) ? { + place: `${plugin}.need_modify()`, + message: 'Promise not allowed, FetchEvent.respondWith count not be called async', + promise: error, + } : { place: `${plugin}.modify()`, - message: error.message, + message: (error as Error).message, error, - }) - } + } + ) } } diff --git a/wire/sync/sync.ts b/wire/sync/sync.ts index 90a5a328eb2..0666ecf00ad 100644 --- a/wire/sync/sync.ts +++ b/wire/sync/sync.ts @@ -19,6 +19,13 @@ namespace $ { } }, + + construct(constr, args) { + const obj = (...args: unknown[]) => new (constr as new(...args: unknown[]) => any)(...args) + const temp = $mol_wire_task.getter( obj ) + const fiber = temp( constr, args ) + return fiber.sync() + }, apply( obj, self, args ) { const temp = $mol_wire_task.getter( obj as ( ... args: any[] )=> any ) @@ -37,8 +44,16 @@ namespace $ { [K in keyof Host]: FunctionResultAwaited } + type ConstructorResultAwaited = Some extends (new (...args: infer Args) => infer Res) + ? new (...args: Args) => Awaited + : Some + type ObjectOrFunctionResultAwaited = ( - Some extends (...args: any) => unknown ? FunctionResultAwaited : {} + Some extends new (...args: unknown[]) => unknown + ? ConstructorResultAwaited + : Some extends (...args: unknown[]) => unknown + ? FunctionResultAwaited + : {} ) & ( Some extends Object ? MethodsResultAwaited : Some ) } From 10b0cef96f66936bd40ebd9298abc505320ae0aa Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 3 Nov 2024 16:18:54 +0300 Subject: [PATCH 45/60] $mol_service push event handler added, sync scope --- offline/offline.ts | 3 +- service/message/event/event.ts | 17 +++++- service/message/event/event.web.ts | 22 -------- service/plugin/plugin.ts | 2 + service/worker/worker.web.ts | 83 ++++++++++++++++++++---------- 5 files changed, 74 insertions(+), 53 deletions(-) delete mode 100644 service/message/event/event.web.ts diff --git a/offline/offline.ts b/offline/offline.ts index cc01c326131..71e2d8aa97d 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -12,7 +12,7 @@ namespace $ { } static override activate() { - return this.$.$mol_service_worker.claim() + this.$.$mol_service_worker.claim() } static override need_modify(request: Request) { @@ -27,7 +27,6 @@ namespace $ { static override modify(request: Request) { let fallback_header - const html = request.mode === 'navigate' const cache = request.cache diff --git a/service/message/event/event.ts b/service/message/event/event.ts index e9fd896aed5..53a2be5d40c 100644 --- a/service/message/event/event.ts +++ b/service/message/event/event.ts @@ -1,9 +1,22 @@ namespace $ { export class $mol_service_message_event extends $mol_object { + readonly raw!: { + data: unknown + ports: readonly MessagePort[] + } + data() { - return null as null | Record + const data = this.raw.data as string | null | { + [k: string]: unknown + } + if ( ! data || typeof data !== 'object' ) return null + return data } - result(result: {} | null, errors?: readonly Error[]) {} + @ $mol_action + result(result: {} | null, errors?: readonly Error[]) { + const error = errors?.length ? errors[0].toString() || errors[0].message : null + this.raw.ports[0].postMessage({ error, result }) + } } } diff --git a/service/message/event/event.web.ts b/service/message/event/event.web.ts deleted file mode 100644 index 93e4aa0997b..00000000000 --- a/service/message/event/event.web.ts +++ /dev/null @@ -1,22 +0,0 @@ -namespace $ { - export class $mol_service_message_event_web extends $mol_service_message_event { - event!: ExtendableMessageEvent - - override data() { - const data = this.event.data as string | null | { - [k: string]: unknown - } - if ( ! data || typeof data !== 'object' ) return null - return data - } - - @ $mol_action - override result(result: {} | null, errors?: readonly Error[]) { - const error = errors?.length ? errors[0].toString() || errors[0].message : null - this.event.ports[0].postMessage({ error, result }) - } - - } - - $.$mol_service_message_event = $mol_service_message_event_web -} diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 4c665e00f3b..b2ad3338353 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -31,6 +31,8 @@ namespace $ { export class $mol_service_plugin_notify extends $mol_service_plugin_base { static notification(e: unknown) { } + static notification_close(e: unknown) { } + static push(data: null | { json() : {}}) {} } } diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index 658ab50ee5d..c7ca125c859 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -65,36 +65,34 @@ namespace $ { static send(data: {}) { return this.post_message(data).result() } + static handle): void }>( cb: (e: E) => void ) { + + return $mol_func_name_from((event: E) => { + event.waitUntil($mol_wire_async(cb)(event)) + }, cb) + + } + @ $mol_mem protected static scope() { const scope = self as unknown as ServiceWorkerGlobalScope - scope.addEventListener( 'install' , event => { - // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting#examples - scope.skipWaiting() - event.waitUntil($mol_wire_async(this).install(event)) - }) + scope.addEventListener( 'install' , this.handle(this.install.bind(this))) + scope.addEventListener( 'activate' , this.handle(this.activate.bind(this))) - scope.addEventListener( 'activate' , event => { - event.waitUntil($mol_wire_async(this).activate(event)) - }) + scope.addEventListener( 'message', this.handle(this.message.bind(this))) + scope.addEventListener( 'messageerror', this.message_error.bind(this)) - scope.addEventListener( 'message', event => { - event.waitUntil( $mol_wire_async(this).message( - $mol_service_message_event_web.make({ event }) - ) ) - }) + scope.addEventListener( 'notificationclick', this.handle(this.notify.bind(this, false))) + scope.addEventListener( 'notificationclose', this.handle(this.notify.bind(this, true))) + scope.addEventListener( 'push', this.handle(this.push.bind(this))) scope.addEventListener( 'fetch', event => { const response = this.fetch(event) if (response) event.respondWith(response) }) - scope.addEventListener( 'notificationclick', event => { - event.waitUntil($mol_wire_async(this).notification_click(event)) - }) - - return scope + return $mol_wire_sync(scope) } protected static clients() { @@ -125,11 +123,22 @@ namespace $ { return this.clients().openWindow(url) } - static message(event: $mol_service_message_event) { - const data = event.data() - if (! data) return - let errors = [] + static message_error(event: MessageEvent) { + const error = new $mol_error_mix('Message deserialization failed', { cause: event }) + console.error(error) + const port = event.ports[0] + port?.postMessage({ result: null, error: error.toString() }) + } + + static message(event: ExtendableMessageEvent) { + const data = event.data as string | null | { [k: string]: unknown } + const port = event.ports[0] + if ( ! data || typeof data !== 'object' ) { + const error = data ? 'Message data empty' : 'Message data is not object' + return port?.postMessage({ error, result: null }) + } + let result for (const plugin of this.plugins()) { @@ -143,17 +152,21 @@ namespace $ { message: error.message, error, }) - errors.push(error) + port?.postMessage({ error: error.toString(), result: null }) + return null } } } - event.result(result ?? null, errors) - + port?.postMessage({ error: null, result }) return null } static install(event: ExtendableEvent) { + // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting#examples + // https://github.com/GoogleChrome/samples/blob/master/service-worker/prefetch-video/service-worker.js#L46 + this.scope().skipWaiting() + for (const plugin of this.plugins()) { try { plugin.install() @@ -207,10 +220,26 @@ namespace $ { return this.plugins_raw().filter(plugin => $mol_service_plugin_cache.is(plugin)) } - static notification_click(event: NotificationEvent) { + static push(event: PushEvent) { + for (const plugin of this.plugins_notify()) { + try { + plugin.push(event.data) + } catch (error) { + if ($mol_fail_catch(error)) { + this.$.$mol_log3_fail({ + place: `${plugin}.push()`, + message: error.message, + error, + }) + } + } + } + } + + static notify(closing: boolean, event: NotificationEvent) { for (const plugin of this.plugins_notify()) { try { - plugin.notification(event.notification) + closing ? plugin.notification_close(event.notification) : plugin.notification(event.notification) } catch (error) { if ($mol_fail_catch(error)) { this.$.$mol_log3_fail({ From 1efed9b21742970fc1686c4df4cd91bc3e026a81 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 3 Nov 2024 19:37:58 +0300 Subject: [PATCH 46/60] $mol_service_worker refactor, extracted prompt events --- notify/notify.web.ts | 2 +- offline/offline.ts | 7 ++ service/message/event/event.ts | 22 ------ service/prepare/event.ts | 10 --- service/prompt/prompt.web.ts | 40 ++++++++++ service/worker/worker.ts | 12 --- service/worker/worker.web.ts | 138 ++++++++++++--------------------- 7 files changed, 97 insertions(+), 134 deletions(-) delete mode 100644 service/message/event/event.ts delete mode 100644 service/prepare/event.ts create mode 100644 service/prompt/prompt.web.ts diff --git a/notify/notify.web.ts b/notify/notify.web.ts index 5e9eff39c33..c874cfddf17 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -50,7 +50,7 @@ namespace $ { } static override notification( notification: Notification ) { - const matched = this.$.$mol_service_worker_web.clients_filter({ includeUncontrolled: true, type: 'window' }) + const matched = this.$.$mol_service_worker_web.clients_grab({ includeUncontrolled: true, type: 'window' }) const last = matched.at(-1) if( last ) { diff --git a/offline/offline.ts b/offline/offline.ts index 71e2d8aa97d..8e046552db1 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -5,6 +5,11 @@ namespace $ { '//cse.google.com/adsense/search/async-ads.js' ] + @ $mol_mem + static prompt() { + this.$.$mol_service_prompt.last()?.prompt() + } + static override blocked( request: Request ) { const normalized_url = request.url.replace( /^https?:/, '' ) @@ -63,6 +68,8 @@ namespace $ { } } + $mol_offline.prompt() + export namespace $mol_service_plugin { export let $mol_offline = $.$mol_offline } diff --git a/service/message/event/event.ts b/service/message/event/event.ts deleted file mode 100644 index 53a2be5d40c..00000000000 --- a/service/message/event/event.ts +++ /dev/null @@ -1,22 +0,0 @@ -namespace $ { - export class $mol_service_message_event extends $mol_object { - readonly raw!: { - data: unknown - ports: readonly MessagePort[] - } - - data() { - const data = this.raw.data as string | null | { - [k: string]: unknown - } - if ( ! data || typeof data !== 'object' ) return null - return data - } - - @ $mol_action - result(result: {} | null, errors?: readonly Error[]) { - const error = errors?.length ? errors[0].toString() || errors[0].message : null - this.raw.ports[0].postMessage({ error, result }) - } - } -} diff --git a/service/prepare/event.ts b/service/prepare/event.ts deleted file mode 100644 index d1bc9c53e9a..00000000000 --- a/service/prepare/event.ts +++ /dev/null @@ -1,10 +0,0 @@ -namespace $ { - export interface $mol_service_prepare_event { - preventDefault(): void - prompt(): Promise - platforms: readonly string[] - userChoise: Promise<{ - outcome: 'accepted' | 'dismissed' - }> - } -} diff --git a/service/prompt/prompt.web.ts b/service/prompt/prompt.web.ts new file mode 100644 index 00000000000..99d5dd8aba9 --- /dev/null +++ b/service/prompt/prompt.web.ts @@ -0,0 +1,40 @@ +namespace $ { + // https://web.dev/learn/pwa/installation-prompt + interface BeforeInstallEvent extends Event { + prompt(): Promise + platforms: readonly string[] + userChoise: Promise<{ + outcome: 'accepted' | 'dismissed' + }> + } + + export class $mol_service_prompt extends $mol_object { + @ $mol_mem + protected static event() { + const install = (event: Event) => { + this.last(new this.$.$mol_service_prompt(event as BeforeInstallEvent)) + } + + const win = this.$.$mol_dom_context + win.addEventListener( 'beforeinstallprompt' , install) + + return { + destructor: () => win.removeEventListener('beforeinstallprompt', install) + } + } + + @ $mol_mem + static last(next?: $mol_service_prompt) { + this.event() + return next ?? null + } + platforms() { return this.native.platforms } + + constructor(readonly native: BeforeInstallEvent) { super() } + + prompt() { return $mol_wire_sync(this.native).prompt() } + choise_async() { return this.native.userChoise } + choise() { return $mol_wire_sync(this).choise_async() } + } + +} diff --git a/service/worker/worker.ts b/service/worker/worker.ts index 2bd7cb2f52f..92896e446c2 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -6,20 +6,8 @@ namespace $ { static send(data: {}) { return null as unknown } static init() {} - - @ $mol_mem - static prepare(next?: $mol_service_prepare_event) { - next?.prompt() - return next ?? null - } - static claim() {} - @ $mol_mem - static prepare_choise() { return $mol_wire_sync(this).choise_promise()?.outcome ?? null } - - static choise_promise() { return this.prepare()?.userChoise } - static blocked_response() { return new Response( null, diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index c7ca125c859..75cb294f0b1 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -31,8 +31,6 @@ namespace $ { throw new Error( 'HTTPS or localhost is required for service workers.' ) } - win.addEventListener( 'beforeinstallprompt' , this.prepare.bind(this) as unknown as (e: Event) => unknown ) - return $mol_wire_sync(win.navigator.serviceWorker) } @@ -102,64 +100,65 @@ namespace $ { static override claim() { return this.clients().claim() } @ $mol_mem_key - static client(id: string) { - const client = this.clients().get(id) + static client(id: string, next?: Cli) { + const client = next ?? (this.clients().get(id) as Cli) return client ? $mol_wire_sync(client) : null } @ $mol_action - static clients_filter( - query?: Query - ) { + static clients_grab(query?: Query) { return this.clients().matchAll(query) - .map(client => $mol_wire_sync(client)) as unknown as readonly ( - Query['type'] extends 'window' - ? WindowClient - : Client - )[] + .map(client => this.client(client.id, client as Query extends 'window' ? WindowClient : Client)!) } static window_open(url: string | URL) { return this.clients().openWindow(url) } - static message_error(event: MessageEvent) { - const error = new $mol_error_mix('Message deserialization failed', { cause: event }) - console.error(error) + const message = 'Message deserialization failed' + + this.$.$mol_log3_fail({ place: `${this}.message_error()`, message }) + const port = event.ports[0] - port?.postMessage({ result: null, error: error.toString() }) + port?.postMessage({ result: null, error: message }) } static message(event: ExtendableMessageEvent) { const data = event.data as string | null | { [k: string]: unknown } const port = event.ports[0] + if ( ! data || typeof data !== 'object' ) { const error = data ? 'Message data empty' : 'Message data is not object' return port?.postMessage({ error, result: null }) } - let result - for (const plugin of this.plugins()) { try { const result = plugin.data(data) - if (result) break + if (! result) continue + port?.postMessage({ error: null, result }) + return } catch (error) { - if ( $mol_fail_catch(error) ) { - this.$.$mol_log3_fail({ - place: `${plugin}.data()`, - message: error.message, - error, - }) - port?.postMessage({ error: error.toString(), result: null }) - return null - } + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.data()`) + port?.postMessage({ error: error.toString(), result: null }) + return } } - port?.postMessage({ error: null, result }) - return null + } + + static plugin_error(error: unknown, place: string) { + if ($mol_promise_like(error)) { + error = new Error('Promise not allowed', { cause: error }) + } + + this.$.$mol_log3_fail({ + place, + message: (error as Error).message, + error, + }) } static install(event: ExtendableEvent) { @@ -171,13 +170,8 @@ namespace $ { try { plugin.install() } catch (error) { - if ($mol_fail_catch(error)) { - this.$.$mol_log3_fail({ - place: `${plugin}.install()`, - message: error.message, - error, - }) - } + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.install()`) } } } @@ -187,13 +181,8 @@ namespace $ { try { plugin.activate() } catch (error) { - if ($mol_fail_catch(error)) { - this.$.$mol_log3_fail({ - place: `${plugin}.activate()`, - message: error.message, - error, - }) - } + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.activate()`) } } @@ -225,13 +214,8 @@ namespace $ { try { plugin.push(event.data) } catch (error) { - if ($mol_fail_catch(error)) { - this.$.$mol_log3_fail({ - place: `${plugin}.push()`, - message: error.message, - error, - }) - } + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.push()`) } } } @@ -241,57 +225,33 @@ namespace $ { try { closing ? plugin.notification_close(event.notification) : plugin.notification(event.notification) } catch (error) { - if ($mol_fail_catch(error)) { - this.$.$mol_log3_fail({ - place: `${plugin}.notification()`, - message: error.message, - error, - }) - } + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.${closing ? 'notification_close' : 'notification'}()`) } } } - @ $mol_action - static block(request: Request) { + static fetch(event: FetchEvent) { for (const plugin of this.plugins_cache()) { try { - if (plugin.blocked(request)) { - return this.blocked_response() - } + if (! plugin.blocked(event.request)) continue + return this.blocked_response() } catch (error) { - if ($mol_fail_catch(error)) { - this.$.$mol_log3_fail({ - place: `${plugin}.blocked()`, - message: error.message, - error, - }) - } + this.plugin_error(error, `${plugin}.blocked()`) } } - return null - } - - static fetch(event: FetchEvent) { - const response = this.block(event.request) - if (response) return response for (const plugin of this.plugins_cache()) { try { - if (plugin.need_modify(event.request)) { - return $mol_wire_async(plugin).modify(event.request) - } + if (! plugin.need_modify(event.request)) continue + + return $mol_wire_async(plugin).modify(event.request) + .catch(error => { + this.plugin_error(error, `${plugin}.need_modify()`) + throw error + }) } catch (error) { - this.$.$mol_log3_fail($mol_promise_like(error) ? { - place: `${plugin}.need_modify()`, - message: 'Promise not allowed, FetchEvent.respondWith count not be called async', - promise: error, - } : { - place: `${plugin}.modify()`, - message: (error as Error).message, - error, - } - ) + this.plugin_error(error, `${plugin}.need_modify()`) } } From 7f008e6bdec6bd86fda7e3fe8a3db96152e9b435 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 3 Nov 2024 19:43:31 +0300 Subject: [PATCH 47/60] $mol_service_worker type fix --- service/prompt/prompt.web.ts | 1 - service/worker/worker.web.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/service/prompt/prompt.web.ts b/service/prompt/prompt.web.ts index 99d5dd8aba9..3628b118ba9 100644 --- a/service/prompt/prompt.web.ts +++ b/service/prompt/prompt.web.ts @@ -28,7 +28,6 @@ namespace $ { this.event() return next ?? null } - platforms() { return this.native.platforms } constructor(readonly native: BeforeInstallEvent) { super() } diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index 75cb294f0b1..ac7cd48eba8 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -108,7 +108,7 @@ namespace $ { @ $mol_action static clients_grab(query?: Query) { return this.clients().matchAll(query) - .map(client => this.client(client.id, client as Query extends 'window' ? WindowClient : Client)!) + .map(client => this.client(client.id, client as Query['type'] extends 'window' ? WindowClient : Client)!) } static window_open(url: string | URL) { From 216976957fa9aa7d272a005dbea3e184e13538b6 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 3 Nov 2024 19:57:02 +0300 Subject: [PATCH 48/60] $mol_service_worker simple notify --- service/registration/registration.web.ts | 4 ---- service/worker/worker.web.ts | 10 +++++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/service/registration/registration.web.ts b/service/registration/registration.web.ts index c6e56dc00f0..292356a477b 100644 --- a/service/registration/registration.web.ts +++ b/service/registration/registration.web.ts @@ -32,10 +32,6 @@ namespace $ { })) } - protected container() { - return this.$.$mol_dom_context.navigator?.serviceWorker ?? $mol_fail(new Error('Service worker not found')) - } - @ $mol_mem state(reset?: null) { return this.worker().state ?? null } diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index ac7cd48eba8..ac8253aa55e 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -81,8 +81,8 @@ namespace $ { scope.addEventListener( 'message', this.handle(this.message.bind(this))) scope.addEventListener( 'messageerror', this.message_error.bind(this)) - scope.addEventListener( 'notificationclick', this.handle(this.notify.bind(this, false))) - scope.addEventListener( 'notificationclose', this.handle(this.notify.bind(this, true))) + scope.addEventListener( 'notificationclick', this.handle(this.notify.bind(this, 'notification'))) + scope.addEventListener( 'notificationclose', this.handle(this.notify.bind(this, 'notification_close'))) scope.addEventListener( 'push', this.handle(this.push.bind(this))) scope.addEventListener( 'fetch', event => { @@ -220,13 +220,13 @@ namespace $ { } } - static notify(closing: boolean, event: NotificationEvent) { + static notify(method: 'notification' | 'notification_close', event: NotificationEvent) { for (const plugin of this.plugins_notify()) { try { - closing ? plugin.notification_close(event.notification) : plugin.notification(event.notification) + plugin[method](event.notification) } catch (error) { if ( ! $mol_fail_catch(error) ) continue - this.plugin_error(error, `${plugin}.${closing ? 'notification_close' : 'notification'}()`) + this.plugin_error(error, `${plugin}.${method}()`) } } } From 2265c73d83bd226147859fd488b1995c57887cef Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 3 Nov 2024 20:04:25 +0300 Subject: [PATCH 49/60] $mol_service_prompt moved to offline, refactor --- offline/offline.ts | 4 ++-- {service => offline}/prompt/prompt.web.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename {service => offline}/prompt/prompt.web.ts (83%) diff --git a/offline/offline.ts b/offline/offline.ts index 8e046552db1..c226ea0005a 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -7,7 +7,7 @@ namespace $ { @ $mol_mem static prompt() { - this.$.$mol_service_prompt.last()?.prompt() + this.$.$mol_offline_prompt.last()?.prompt() } static override blocked( request: Request ) { @@ -68,7 +68,7 @@ namespace $ { } } - $mol_offline.prompt() + new $mol_after_tick(() => $mol_offline.prompt()) export namespace $mol_service_plugin { export let $mol_offline = $.$mol_offline diff --git a/service/prompt/prompt.web.ts b/offline/prompt/prompt.web.ts similarity index 83% rename from service/prompt/prompt.web.ts rename to offline/prompt/prompt.web.ts index 3628b118ba9..24b6f95780e 100644 --- a/service/prompt/prompt.web.ts +++ b/offline/prompt/prompt.web.ts @@ -8,11 +8,11 @@ namespace $ { }> } - export class $mol_service_prompt extends $mol_object { + export class $mol_offline_prompt extends $mol_object { @ $mol_mem protected static event() { const install = (event: Event) => { - this.last(new this.$.$mol_service_prompt(event as BeforeInstallEvent)) + this.last(new this.$.$mol_offline_prompt(event as BeforeInstallEvent)) } const win = this.$.$mol_dom_context @@ -24,7 +24,7 @@ namespace $ { } @ $mol_mem - static last(next?: $mol_service_prompt) { + static last(next?: $mol_offline_prompt) { this.event() return next ?? null } From 9dafc68ab9463a9de96f7d6c4f4420bfa0aec6b2 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Sun, 3 Nov 2024 20:08:31 +0300 Subject: [PATCH 50/60] $mol_offline web --- offline/offline.ts | 7 ------- offline/offline.web.ts | 13 +++++++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 offline/offline.web.ts diff --git a/offline/offline.ts b/offline/offline.ts index c226ea0005a..71e2d8aa97d 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -5,11 +5,6 @@ namespace $ { '//cse.google.com/adsense/search/async-ads.js' ] - @ $mol_mem - static prompt() { - this.$.$mol_offline_prompt.last()?.prompt() - } - static override blocked( request: Request ) { const normalized_url = request.url.replace( /^https?:/, '' ) @@ -68,8 +63,6 @@ namespace $ { } } - new $mol_after_tick(() => $mol_offline.prompt()) - export namespace $mol_service_plugin { export let $mol_offline = $.$mol_offline } diff --git a/offline/offline.web.ts b/offline/offline.web.ts new file mode 100644 index 00000000000..95182ac4ea2 --- /dev/null +++ b/offline/offline.web.ts @@ -0,0 +1,13 @@ +namespace $ { + export class $mol_offline_web extends $mol_offline { + @ $mol_mem + static prompt() { + this.$.$mol_offline_prompt.last()?.prompt() + } + } + + $.$mol_offline = $mol_offline_web + $mol_service_plugin.$mol_offline = $.$mol_offline_web + + new $mol_after_tick(() => $mol_offline_web.prompt()) +} From 7778411915963659cc0854d9c5831de4e1ea06ff Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Mon, 4 Nov 2024 16:57:49 +0300 Subject: [PATCH 51/60] $mol_service_worker fix start --- service/plugin/plugin.ts | 2 +- service/registration/registration.web.ts | 4 +++- service/worker/worker.ts | 6 ++++++ service/worker/worker.web.ts | 12 ++---------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index b2ad3338353..1428392c237 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -1,5 +1,5 @@ namespace $ { - $mol_service_worker.init() + $mol_service_worker.start() export namespace $mol_service_plugin { let _ diff --git a/service/registration/registration.web.ts b/service/registration/registration.web.ts index 292356a477b..27fc6ec6a2d 100644 --- a/service/registration/registration.web.ts +++ b/service/registration/registration.web.ts @@ -22,7 +22,9 @@ namespace $ { @ $mol_mem worker(reset?: null) { const reg = this.native() - const worker = reg.installing ?? reg.waiting ?? reg.active ?? $mol_fail(new Error('No worker in registration')) + const worker = reg.installing ?? reg.waiting ?? reg.active + ?? (self as unknown as ServiceWorkerGlobalScope).serviceWorker + ?? $mol_fail(new Error('No worker in registration')) const state_reset = () => this.state(null) worker.addEventListener( 'statechange', state_reset) diff --git a/service/worker/worker.ts b/service/worker/worker.ts index 92896e446c2..3f2044892c4 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -6,6 +6,12 @@ namespace $ { static send(data: {}) { return null as unknown } static init() {} + + static async start() { + await Promise.resolve() + await $mol_wire_async(this).init() + } + static claim() {} static blocked_response() { diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index ac8253aa55e..f14855e7b7a 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -3,20 +3,12 @@ namespace $ { export class $mol_service_worker_web extends $mol_service_worker { - static plugins_add_wait() { return Promise.resolve() } @ $mol_mem static init() { $mol_wire_solid() - try { - if ( this.$.$mol_service_ensure() ) { - $mol_wire_sync(this).plugins_add_wait() - this.plugins() - } - this.registration().worker() - } catch (error) { - this.$.$mol_fail_log(error) - } + if ( this.$.$mol_service_ensure() ) this.plugins() + this.registration().worker() return null } From a2aa4c3b49b0a9b52b15fe90e6be3478e7928abd Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Mon, 4 Nov 2024 20:29:53 +0300 Subject: [PATCH 52/60] $mol_offline fix prompt offline --- offline/offline.web.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/offline/offline.web.ts b/offline/offline.web.ts index 95182ac4ea2..ad86866e0e5 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -9,5 +9,7 @@ namespace $ { $.$mol_offline = $mol_offline_web $mol_service_plugin.$mol_offline = $.$mol_offline_web - new $mol_after_tick(() => $mol_offline_web.prompt()) + if (! $mol_service_ensure()) { + new $mol_after_tick(() => $mol_offline_web.prompt()) + } } From 6715f91b453ec3c20a325b76fea6828923b56c31 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 5 Nov 2024 10:52:57 +0300 Subject: [PATCH 53/60] $mol_service_worker minor fixes --- offline/offline.ts | 4 ++-- service/plugin/plugin.ts | 4 ---- service/worker/worker.web.ts | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/offline/offline.ts b/offline/offline.ts index 71e2d8aa97d..e2efbefd34d 100644 --- a/offline/offline.ts +++ b/offline/offline.ts @@ -33,7 +33,7 @@ namespace $ { if (cache === 'reload' || ( cache === 'no-cache' && ! html ) ) { if (cache === 'reload') { // F5 + Disable cache - request = this.request_clone(request, { cache: 'no-cache' }) + request = new ($mol_wire_sync(Request))(request, { cache: 'no-cache' }) } // fetch with fallback to cache if statuses not match @@ -49,7 +49,7 @@ namespace $ { } if (cache !== 'force-cache') { - request = this.request_clone(request, { cache: 'force-cache' }) + request = new ($mol_wire_sync(Request))(request, { cache: 'force-cache' }) } const cached = this.$.$mol_fetch.response(request) diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 1428392c237..1bbb7b85f0a 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -23,10 +23,6 @@ namespace $ { static blocked(request: Request) { return false } static need_modify(request: Request) { return false } static modify(request: Request) { return new Response } - @ $mol_action - protected static request_clone(original: Request, options?: RequestInit) { - return new Request(original, options) - } } export class $mol_service_plugin_notify extends $mol_service_plugin_base { diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index f14855e7b7a..2b02e942951 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -239,7 +239,7 @@ namespace $ { return $mol_wire_async(plugin).modify(event.request) .catch(error => { - this.plugin_error(error, `${plugin}.need_modify()`) + this.plugin_error(error, `${plugin}.modify()`) throw error }) } catch (error) { From 001e5626b0dd7d27fe8ad5863dde7a2fbdf4e2e1 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Tue, 5 Nov 2024 12:21:01 +0300 Subject: [PATCH 54/60] $mol_fetch removed unused request --- fetch/fetch.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fetch/fetch.ts b/fetch/fetch.ts index 89734037ddc..76cdcc7c4cb 100644 --- a/fetch/fetch.ts +++ b/fetch/fetch.ts @@ -80,10 +80,6 @@ namespace $ { } } - export function $mol_fetch_request_clone(this: typeof $, original: Request, options?: RequestInit) { - return new Request(original, options) - } - $.$mol_fetch_request_clone = $mol_wire_sync($).$mol_fetch_request_clone export class $mol_fetch extends $mol_object2 { From 60811c152a5f65e372cbc9d8ffcef6bc20001407 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Wed, 6 Nov 2024 11:41:19 +0300 Subject: [PATCH 55/60] $mol_service refactor, split self and worker --- notify/notify.web.ts | 9 +- offline/offline.web.ts | 4 +- service/ensure/ensure.ts | 5 - service/plugin/plugin.ts | 6 +- service/process/process.ts | 5 + service/registration/registration.web.ts | 41 ---- service/self/self.ts | 21 ++ service/self/self.web.ts | 224 ++++++++++++++++++++ service/worker/worker.ts | 9 +- service/worker/worker.web.ts | 248 ++++------------------- 10 files changed, 308 insertions(+), 264 deletions(-) delete mode 100644 service/ensure/ensure.ts create mode 100644 service/process/process.ts delete mode 100644 service/registration/registration.web.ts create mode 100644 service/self/self.ts create mode 100644 service/self/self.web.ts diff --git a/notify/notify.web.ts b/notify/notify.web.ts index c874cfddf17..f598e2b8999 100644 --- a/notify/notify.web.ts +++ b/notify/notify.web.ts @@ -31,9 +31,8 @@ namespace $ { export class $mol_notify_service_web extends $mol_notify_service { static override show({ context: title, message: body, uri: data }: $mol_notify_info) { - const registration = this.$.$mol_service_worker_web.registration().native() const tag = data - const existen = registration.getNotifications({ tag }) + const existen = this.$.$mol_service_self_web.notifications({ tag }) for( const not of existen ) { @@ -45,12 +44,12 @@ namespace $ { // const vibrate = [ 100, 200, 300, 400, 500 ] - registration.showNotification( title, { body, data, /*vibrate,*/ tag } ) + this.$.$mol_service_self_web.notification_show( title, { body, data, /*vibrate,*/ tag } ) } static override notification( notification: Notification ) { - const matched = this.$.$mol_service_worker_web.clients_grab({ includeUncontrolled: true, type: 'window' }) + const matched = this.$.$mol_service_self_web.clients_grab({ includeUncontrolled: true, type: 'window' }) const last = matched.at(-1) if( last ) { @@ -60,7 +59,7 @@ namespace $ { return null } - this.$.$mol_service_worker_web.window_open( notification.data ) + this.$.$mol_service_self_web.window_open( notification.data ) return null } } diff --git a/offline/offline.web.ts b/offline/offline.web.ts index ad86866e0e5..4ad7cdd08a1 100644 --- a/offline/offline.web.ts +++ b/offline/offline.web.ts @@ -9,7 +9,7 @@ namespace $ { $.$mol_offline = $mol_offline_web $mol_service_plugin.$mol_offline = $.$mol_offline_web - if (! $mol_service_ensure()) { - new $mol_after_tick(() => $mol_offline_web.prompt()) + if ( $mol_service_process()) { + Promise.resolve().then(() => $mol_wire_async($mol_offline_web).prompt()) } } diff --git a/service/ensure/ensure.ts b/service/ensure/ensure.ts deleted file mode 100644 index 78b46e0a8b4..00000000000 --- a/service/ensure/ensure.ts +++ /dev/null @@ -1,5 +0,0 @@ -namespace $ { - export function $mol_service_ensure() { - return typeof window === 'undefined' - } -} diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 1bbb7b85f0a..4471661edd8 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -1,5 +1,9 @@ namespace $ { - $mol_service_worker.start() + if ( $mol_service_process() ) { + Promise.resolve().then(() => $mol_wire_async($.$mol_service_self).init()) + } else { + $mol_wire_async($.$mol_service_worker).init() + } export namespace $mol_service_plugin { let _ diff --git a/service/process/process.ts b/service/process/process.ts new file mode 100644 index 00000000000..76d78b9a09e --- /dev/null +++ b/service/process/process.ts @@ -0,0 +1,5 @@ +namespace $ { + export function $mol_service_process() { + return typeof self !== 'undefined' && Boolean((self as unknown as { serviceWorker?: {} }).serviceWorker) + } +} diff --git a/service/registration/registration.web.ts b/service/registration/registration.web.ts deleted file mode 100644 index 27fc6ec6a2d..00000000000 --- a/service/registration/registration.web.ts +++ /dev/null @@ -1,41 +0,0 @@ -namespace $ { - export class $mol_service_registration_web extends $mol_object { - constructor ( protected raw: ServiceWorkerRegistration ) { super() } - - @ $mol_mem - native() { - const reg = this.raw - // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/updatefound_event - const worker_reset = () => this.worker(null) - reg.addEventListener( 'updatefound', worker_reset) - - return $mol_wire_sync(Object.assign(reg, { - destructor: () => { - reg.removeEventListener( 'updatefound', worker_reset) - } - })) - } - - preload() { return $mol_wire_sync(this.native().navigationPreload) } - pushes() { return $mol_wire_sync(this.native().pushManager) } - - @ $mol_mem - worker(reset?: null) { - const reg = this.native() - const worker = reg.installing ?? reg.waiting ?? reg.active - ?? (self as unknown as ServiceWorkerGlobalScope).serviceWorker - ?? $mol_fail(new Error('No worker in registration')) - - const state_reset = () => this.state(null) - worker.addEventListener( 'statechange', state_reset) - - return $mol_wire_sync(Object.assign(worker, { - destructor: () => worker.removeEventListener('statechange', state_reset) - })) - } - - @ $mol_mem - state(reset?: null) { return this.worker().state ?? null } - - } -} diff --git a/service/self/self.ts b/service/self/self.ts new file mode 100644 index 00000000000..f95277f76c9 --- /dev/null +++ b/service/self/self.ts @@ -0,0 +1,21 @@ +namespace $ { + /** + * Со стороны + */ + export class $mol_service_self extends $mol_object { + static init() {} + + static claim() {} + + static blocked_response() { + return new Response( + null, + { + status: 418, + statusText: 'Blocked' + }, + ) + } + } + +} diff --git a/service/self/self.web.ts b/service/self/self.web.ts new file mode 100644 index 00000000000..049f4b35c93 --- /dev/null +++ b/service/self/self.web.ts @@ -0,0 +1,224 @@ +namespace $ { + export class $mol_service_self_web extends $mol_service_self { + static handle): void }>( cb: (e: E) => void ) { + + return $mol_func_name_from((event: E) => { + event.waitUntil($mol_wire_async(cb)(event)) + }, cb) + + } + + static override init() { this.scope() } + + @ $mol_mem + protected static scope() { + const scope = self as unknown as ServiceWorkerGlobalScope + + const handlers = { + install: this.handle(this.install.bind(this)), + activate: this.handle(this.activate.bind(this)), + message: this.handle(this.message.bind(this)), + + messageerror: this.message_error.bind(this), + + notificationclick: this.handle(this.notify.bind(this, 'notification')), + notificationclose: this.handle(this.notify.bind(this, 'notification_close')), + push: this.handle(this.push.bind(this)), + + fetch: this.fetch.bind(this), + } as const + + Object.entries(handlers).forEach(([name, cb]) => { + scope.addEventListener(name, cb as (e: Event) => void) + }) + + return Object.assign(scope, { + destructor: () => { + Object.entries(handlers).forEach(([name, cb]) => { + scope.removeEventListener(name, cb as (e: Event) => void) + }) + } + }) + } + + protected static reg() { return this.scope().registration } + + static preload() { return $mol_wire_sync(this.reg().navigationPreload) } + static pushes() { return $mol_wire_sync(this.reg().pushManager) } + + static notifications(params?: GetNotificationOptions) { + return $mol_wire_sync(this.reg()).getNotifications(params) + } + + static notification_show(title: string, options?: NotificationOptions) { + return $mol_wire_sync(this.reg()).showNotification(title, options) + } + + protected static clients() { + return $mol_wire_sync(this.scope().clients) + } + + static claim() { return this.clients().claim() } + + @ $mol_mem_key + static client(id: string, next?: Cli) { + const client = next ?? (this.clients().get(id) as Cli) + return client ? $mol_wire_sync(client) : null + } + + @ $mol_action + static clients_grab(query?: Query) { + return this.clients().matchAll(query) + .map(client => this.client(client.id, client as Query['type'] extends 'window' ? WindowClient : Client)!) + } + + static window_open(url: string | URL) { + return this.clients().openWindow(url) + } + + static message_error(event: MessageEvent) { + const message = 'Message deserialization failed' + + this.$.$mol_log3_fail({ place: `${this}.message_error()`, message }) + + const port = event.ports[0] + port?.postMessage({ result: null, error: message }) + } + + static message(event: ExtendableMessageEvent) { + const data = event.data as string | null | { [k: string]: unknown } + const port = event.ports[0] + + if ( ! data || typeof data !== 'object' ) { + const error = data ? 'Message data empty' : 'Message data is not object' + return port?.postMessage({ error, result: null }) + } + + for (const plugin of this.plugins()) { + try { + const result = plugin.data(data) + if (! result) continue + port?.postMessage({ error: null, result }) + return + } catch (error) { + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.data()`) + port?.postMessage({ error: error.toString(), result: null }) + return + } + } + + } + + static plugin_error(error: unknown, place: string) { + if ($mol_promise_like(error)) { + error = new Error('Promise not allowed', { cause: error }) + } + + this.$.$mol_log3_fail({ + place, + message: (error as Error).message, + error, + }) + } + + protected static install(event: ExtendableEvent) { + // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting#examples + // https://github.com/GoogleChrome/samples/blob/master/service-worker/prefetch-video/service-worker.js#L46 + this.scope().skipWaiting() + + for (const plugin of this.plugins()) { + try { + plugin.install() + } catch (error) { + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.install()`) + } + } + } + + protected static activate(event: ExtendableEvent) { + for (const plugin of this.plugins()) { + try { + plugin.activate() + } catch (error) { + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.activate()`) + } + } + + this.$.$mol_log3_done({ + place: `${this}.activate()`, + message: 'Activated', + }) + } + + @ $mol_mem + static plugins_raw() { + return Object.values(this.$.$mol_service_plugin) as readonly { prototype: unknown }[] + } + + static plugins() { + return this.plugins_raw().filter(plugin => $mol_service_plugin_base.is(plugin)) + } + + static plugins_notify() { + return this.plugins_raw().filter(plugin => $mol_service_plugin_notify.is(plugin)) + } + + static plugins_cache() { + return this.plugins_raw().filter(plugin => $mol_service_plugin_cache.is(plugin)) + } + + protected static push(event: PushEvent) { + for (const plugin of this.plugins_notify()) { + try { + plugin.push(event.data) + } catch (error) { + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.push()`) + } + } + } + + protected static notify(method: 'notification' | 'notification_close', event: NotificationEvent) { + for (const plugin of this.plugins_notify()) { + try { + plugin[method](event.notification) + } catch (error) { + if ( ! $mol_fail_catch(error) ) continue + this.plugin_error(error, `${plugin}.${method}()`) + } + } + } + + protected static fetch(event: FetchEvent) { + let target + const request = event.request + + try { + target = this.plugins_cache().find(plugin => plugin.blocked(request)) + if (target) return event.respondWith(this.blocked_response()) + + target = this.plugins_cache().find(plugin => plugin.need_modify(request)) + if (! target) return null + + } catch (error) { + this.plugin_error(error, `${target}.blocked()`) + + return null + } + + const promise = $mol_wire_async(target).modify(request) + .catch(error => { + this.plugin_error(error, `${target}.modify()`) + throw error + }) + event.respondWith(promise) + + return null + } + } + + $.$mol_service_self = $mol_service_self_web +} diff --git a/service/worker/worker.ts b/service/worker/worker.ts index 3f2044892c4..c9d256d39bc 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -1,4 +1,7 @@ namespace $ { + /** + * Со стороны + */ export class $mol_service_worker extends $mol_object { static path() { return 'web.js' } @@ -6,10 +9,8 @@ namespace $ { static send(data: {}) { return null as unknown } static init() {} - - static async start() { - await Promise.resolve() - await $mol_wire_async(this).init() + static state() { + return 'installing' as 'activated' | 'activating' | 'installed' | 'installing' | 'parsed' | 'redundant' } static claim() {} diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index 2b02e942951..855a8862c24 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -4,16 +4,11 @@ namespace $ { export class $mol_service_worker_web extends $mol_service_worker { - @ $mol_mem - static init() { - $mol_wire_solid() - if ( this.$.$mol_service_ensure() ) this.plugins() - this.registration().worker() - return null - } + static override init() { this.state() } @ $mol_mem protected static container() { + if ( this.$.$mol_service_process()) throw new Error('Not for running in service worker') const win = this.$.$mol_dom_context if( ! win.navigator.serviceWorker ) { @@ -27,228 +22,69 @@ namespace $ { } @ $mol_mem - static registration() { - const raw = this.$.$mol_service_ensure() - ? this.scope().registration - : this.container().register( this.path() ) - - return new this.$.$mol_service_registration_web(raw) - } - - static ready_async() { return this.container().ready } - static ready() { return $mol_wire_sync( this ).ready_async() } - - @ $mol_action - protected static post_message(data: unknown) { - if (this.$.$mol_service_ensure()) { - throw new Error('Worker can\'t send messages, use clients api') - } - - const channel = this.$.$mol_service_channel.make({ - timeout: () => this.send_timeout() - }) + protected static reg() { + const reg = this.container().register( this.path() ) - this.ready().active?.postMessage(data, [ channel.out() ]) + // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/updatefound_event + const worker_reset = () => this.worker(null) + reg.addEventListener( 'updatefound', worker_reset) - return channel + return $mol_wire_sync(Object.assign(reg, { + destructor: () => { + reg.removeEventListener( 'updatefound', worker_reset) + } + })) } - static send(data: {}) { return this.post_message(data).result() } + static update() { return this.reg().update() } + static unregister() { return this.reg().unregister() } + static update_status() { return this.reg().updateViaCache } + static path() { return this.reg().scope } - static handle): void }>( cb: (e: E) => void ) { + static preload() { return $mol_wire_sync(this.reg().navigationPreload) } + static pushes() { return $mol_wire_sync(this.reg().pushManager) } - return $mol_func_name_from((event: E) => { - event.waitUntil($mol_wire_async(cb)(event)) - }, cb) + static notifications(params?: GetNotificationOptions) { + return $mol_wire_sync(this.reg()).getNotifications(params) + } + static notification_show(title: string, options?: NotificationOptions) { + return $mol_wire_sync(this.reg()).showNotification(title, options) } @ $mol_mem - protected static scope() { - const scope = self as unknown as ServiceWorkerGlobalScope - - scope.addEventListener( 'install' , this.handle(this.install.bind(this))) - scope.addEventListener( 'activate' , this.handle(this.activate.bind(this))) + protected static worker(reset?: null) { + const reg = this.reg() + const worker = reg.installing ?? reg.waiting ?? reg.active + if (! worker) throw new Error('Not for running in service worker') - scope.addEventListener( 'message', this.handle(this.message.bind(this))) - scope.addEventListener( 'messageerror', this.message_error.bind(this)) + const state_reset = () => this.state(null) + worker.addEventListener( 'statechange', state_reset) - scope.addEventListener( 'notificationclick', this.handle(this.notify.bind(this, 'notification'))) - scope.addEventListener( 'notificationclose', this.handle(this.notify.bind(this, 'notification_close'))) - - scope.addEventListener( 'push', this.handle(this.push.bind(this))) - scope.addEventListener( 'fetch', event => { - const response = this.fetch(event) - if (response) event.respondWith(response) - }) - - return $mol_wire_sync(scope) - } - - protected static clients() { - return $mol_wire_sync(this.scope().clients) + return $mol_wire_sync(Object.assign(worker, { + destructor: () => worker.removeEventListener('statechange', state_reset) + })) } - static override claim() { return this.clients().claim() } + @ $mol_mem + static override state(reset?: null) { return this.worker().state } - @ $mol_mem_key - static client(id: string, next?: Cli) { - const client = next ?? (this.clients().get(id) as Cli) - return client ? $mol_wire_sync(client) : null - } + static ready_async() { return this.container().ready } + protected static ready() { return $mol_wire_sync( this ).ready_async() } @ $mol_action - static clients_grab(query?: Query) { - return this.clients().matchAll(query) - .map(client => this.client(client.id, client as Query['type'] extends 'window' ? WindowClient : Client)!) - } - - static window_open(url: string | URL) { - return this.clients().openWindow(url) - } - - static message_error(event: MessageEvent) { - const message = 'Message deserialization failed' - - this.$.$mol_log3_fail({ place: `${this}.message_error()`, message }) - - const port = event.ports[0] - port?.postMessage({ result: null, error: message }) - } - - static message(event: ExtendableMessageEvent) { - const data = event.data as string | null | { [k: string]: unknown } - const port = event.ports[0] - - if ( ! data || typeof data !== 'object' ) { - const error = data ? 'Message data empty' : 'Message data is not object' - return port?.postMessage({ error, result: null }) - } - - for (const plugin of this.plugins()) { - try { - const result = plugin.data(data) - if (! result) continue - port?.postMessage({ error: null, result }) - return - } catch (error) { - if ( ! $mol_fail_catch(error) ) continue - this.plugin_error(error, `${plugin}.data()`) - port?.postMessage({ error: error.toString(), result: null }) - return - } - } - - } - - static plugin_error(error: unknown, place: string) { - if ($mol_promise_like(error)) { - error = new Error('Promise not allowed', { cause: error }) - } - - this.$.$mol_log3_fail({ - place, - message: (error as Error).message, - error, - }) - } - - static install(event: ExtendableEvent) { - // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting#examples - // https://github.com/GoogleChrome/samples/blob/master/service-worker/prefetch-video/service-worker.js#L46 - this.scope().skipWaiting() - - for (const plugin of this.plugins()) { - try { - plugin.install() - } catch (error) { - if ( ! $mol_fail_catch(error) ) continue - this.plugin_error(error, `${plugin}.install()`) - } - } - } - - static activate(event: ExtendableEvent) { - for (const plugin of this.plugins()) { - try { - plugin.activate() - } catch (error) { - if ( ! $mol_fail_catch(error) ) continue - this.plugin_error(error, `${plugin}.activate()`) - } - } - - this.$.$mol_log3_done({ - place: `${this}.activate()`, - message: 'Activated', + protected static post_message(data: unknown) { + const channel = this.$.$mol_service_channel.make({ + timeout: () => this.send_timeout() }) - } - - @ $mol_mem - static plugins_raw() { - return Object.values(this.$.$mol_service_plugin) as readonly { prototype: unknown }[] - } - static plugins() { - return this.plugins_raw().filter(plugin => $mol_service_plugin_base.is(plugin)) - } - - static plugins_notify() { - return this.plugins_raw().filter(plugin => $mol_service_plugin_notify.is(plugin)) - } - - static plugins_cache() { - return this.plugins_raw().filter(plugin => $mol_service_plugin_cache.is(plugin)) - } - - static push(event: PushEvent) { - for (const plugin of this.plugins_notify()) { - try { - plugin.push(event.data) - } catch (error) { - if ( ! $mol_fail_catch(error) ) continue - this.plugin_error(error, `${plugin}.push()`) - } - } - } + this.ready().active?.postMessage(data, [ channel.out() ]) - static notify(method: 'notification' | 'notification_close', event: NotificationEvent) { - for (const plugin of this.plugins_notify()) { - try { - plugin[method](event.notification) - } catch (error) { - if ( ! $mol_fail_catch(error) ) continue - this.plugin_error(error, `${plugin}.${method}()`) - } - } + return channel } - static fetch(event: FetchEvent) { - for (const plugin of this.plugins_cache()) { - try { - if (! plugin.blocked(event.request)) continue - return this.blocked_response() - } catch (error) { - this.plugin_error(error, `${plugin}.blocked()`) - } - } - - for (const plugin of this.plugins_cache()) { - try { - if (! plugin.need_modify(event.request)) continue - - return $mol_wire_async(plugin).modify(event.request) - .catch(error => { - this.plugin_error(error, `${plugin}.modify()`) - throw error - }) - } catch (error) { - this.plugin_error(error, `${plugin}.need_modify()`) - } - } + static send(data: {}) { return this.post_message(data).result() } - return null - } } $.$mol_service_worker = $mol_service_worker_web From d579f8a54f2f3e034b340a5522a19b492de3423a Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Wed, 6 Nov 2024 12:01:54 +0300 Subject: [PATCH 56/60] $mol_service fixes --- service/plugin/plugin.ts | 4 ++-- service/self/self.web.ts | 25 +++++++++++++++++-------- service/worker/worker.ts | 12 +++++++++++- service/worker/worker.web.ts | 4 +--- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 4471661edd8..97ef4ce0c5b 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -1,8 +1,8 @@ namespace $ { if ( $mol_service_process() ) { - Promise.resolve().then(() => $mol_wire_async($.$mol_service_self).init()) + Promise.resolve().then(() => $.$mol_service_self.init()) } else { - $mol_wire_async($.$mol_service_worker).init() + $.$mol_service_worker.init() } export namespace $mol_service_plugin { diff --git a/service/self/self.web.ts b/service/self/self.web.ts index 049f4b35c93..cd9d94d3413 100644 --- a/service/self/self.web.ts +++ b/service/self/self.web.ts @@ -8,7 +8,16 @@ namespace $ { } - static override init() { this.scope() } + @ $mol_mem + static override init() { + try { + this.scope() + } catch (error) { + if ($mol_fail_catch(error)) { + console.error(error) + } + } + } @ $mol_mem protected static scope() { @@ -76,7 +85,7 @@ namespace $ { return this.clients().openWindow(url) } - static message_error(event: MessageEvent) { + protected static message_error(event: MessageEvent) { const message = 'Message deserialization failed' this.$.$mol_log3_fail({ place: `${this}.message_error()`, message }) @@ -85,7 +94,7 @@ namespace $ { port?.postMessage({ result: null, error: message }) } - static message(event: ExtendableMessageEvent) { + protected static message(event: ExtendableMessageEvent) { const data = event.data as string | null | { [k: string]: unknown } const port = event.ports[0] @@ -110,7 +119,7 @@ namespace $ { } - static plugin_error(error: unknown, place: string) { + protected static plugin_error(error: unknown, place: string) { if ($mol_promise_like(error)) { error = new Error('Promise not allowed', { cause: error }) } @@ -154,19 +163,19 @@ namespace $ { } @ $mol_mem - static plugins_raw() { + protected static plugins_raw() { return Object.values(this.$.$mol_service_plugin) as readonly { prototype: unknown }[] } - static plugins() { + protected static plugins() { return this.plugins_raw().filter(plugin => $mol_service_plugin_base.is(plugin)) } - static plugins_notify() { + protected static plugins_notify() { return this.plugins_raw().filter(plugin => $mol_service_plugin_notify.is(plugin)) } - static plugins_cache() { + protected static plugins_cache() { return this.plugins_raw().filter(plugin => $mol_service_plugin_cache.is(plugin)) } diff --git a/service/worker/worker.ts b/service/worker/worker.ts index c9d256d39bc..12699a6a28a 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -8,7 +8,17 @@ namespace $ { static send_timeout() { return 20000 } static send(data: {}) { return null as unknown } - static init() {} + @ $mol_mem + static init() { + try { + this.state() + } catch (error) { + if ($mol_fail_catch(error)) { + console.error(error) + } + } + } + static state() { return 'installing' as 'activated' | 'activating' | 'installed' | 'installing' | 'parsed' | 'redundant' } diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index 855a8862c24..12fe2b3d747 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -4,8 +4,6 @@ namespace $ { export class $mol_service_worker_web extends $mol_service_worker { - static override init() { this.state() } - @ $mol_mem protected static container() { if ( this.$.$mol_service_process()) throw new Error('Not for running in service worker') @@ -39,7 +37,7 @@ namespace $ { static update() { return this.reg().update() } static unregister() { return this.reg().unregister() } static update_status() { return this.reg().updateViaCache } - static path() { return this.reg().scope } + static scope() { return this.reg().scope } static preload() { return $mol_wire_sync(this.reg().navigationPreload) } static pushes() { return $mol_wire_sync(this.reg().pushManager) } From 370372a26e4b7046a887d634e8be1fd88fb1897a Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Wed, 6 Nov 2024 12:14:27 +0300 Subject: [PATCH 57/60] $mol_service fixes 2 --- service/plugin/plugin.ts | 4 ++-- service/worker/worker.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index 97ef4ce0c5b..f25bb8d3f26 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -1,8 +1,8 @@ namespace $ { if ( $mol_service_process() ) { - Promise.resolve().then(() => $.$mol_service_self.init()) + Promise.resolve().then(() => $mol_service_self.init()) } else { - $.$mol_service_worker.init() + $mol_service_worker.init() } export namespace $mol_service_plugin { diff --git a/service/worker/worker.ts b/service/worker/worker.ts index 12699a6a28a..e681dd1ee9d 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -13,7 +13,7 @@ namespace $ { try { this.state() } catch (error) { - if ($mol_fail_catch(error)) { + if (! $mol_promise_like(error)) { console.error(error) } } From a33dcaff2bbfcb4d382a80f60404bf513b33f328 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Thu, 7 Nov 2024 01:32:57 +0300 Subject: [PATCH 58/60] $mol_worker rpc refactored --- dom/listener/listener.ts | 20 ++++++++----- embed/native/native.view.ts | 2 +- rpc/client/client.ts | 57 ++++++++++++++++++++++++++++++++++++ rpc/client/frame/frame.ts | 54 ++++++++++------------------------ rpc/server/iframe/iframe.ts | 11 +++++++ rpc/server/server.ts | 41 ++++++++++++++++---------- rpc/server/worker/worker.ts | 14 +++++++++ service/channel/channel.ts | 49 ------------------------------- service/plugin/plugin.ts | 1 - service/self/self.web.ts | 52 ++++++++++++++------------------ service/worker/worker.ts | 32 +++++++++++++++++--- service/worker/worker.web.ts | 14 ++------- 12 files changed, 189 insertions(+), 158 deletions(-) create mode 100644 rpc/client/client.ts create mode 100644 rpc/server/iframe/iframe.ts create mode 100644 rpc/server/worker/worker.ts delete mode 100644 service/channel/channel.ts diff --git a/dom/listener/listener.ts b/dom/listener/listener.ts index 5463a1da001..cf085cd8285 100644 --- a/dom/listener/listener.ts +++ b/dom/listener/listener.ts @@ -1,19 +1,25 @@ namespace $ { - export class $mol_dom_listener extends $mol_object { + + type Source = { + addEventListener(name: string, handler: (event: EventType) => unknown, passive?: any ): unknown + removeEventListener(name: string, handler: (event: EventType) => unknown, passive?: any ): unknown + } + + export class $mol_dom_listener extends $mol_object { constructor( - public _node : any , - public _event : string , - public _handler : ( event : any )=> any , - public _config : boolean|{ passive : boolean } = { passive : true } + protected node : Source, + protected event : string , + protected handler : (event: EventType) => unknown , + protected config : boolean | { passive : boolean } = { passive : true } ) { super() - this._node.addEventListener( this._event , this._handler , this._config ) + this.node.addEventListener( this.event , this.handler , this.config ) } destructor() { - this._node.removeEventListener( this._event , this._handler , this._config ) + this.node.removeEventListener( this.event , this.handler , this.config ) super.destructor() } diff --git a/embed/native/native.view.ts b/embed/native/native.view.ts index 9fc9abbd8f5..fd6f7494990 100644 --- a/embed/native/native.view.ts +++ b/embed/native/native.view.ts @@ -33,7 +33,7 @@ namespace $.$$ { @ $mol_mem message_listener() { - return new $mol_dom_listener( + return new $mol_dom_listener>( $mol_dom_context, 'message', $mol_wire_async( this ).message_receive diff --git a/rpc/client/client.ts b/rpc/client/client.ts new file mode 100644 index 00000000000..3d63b852909 --- /dev/null +++ b/rpc/client/client.ts @@ -0,0 +1,57 @@ +namespace $ { + export class $mol_rpc_client< Handlers extends Record unknown>> extends $mol_object { + @ $mol_action + protected create_id() { + return `$mol_rpc_client:${this.$.$mol_guid()}:${ Date.now().toString(16) }` + } + + call_async(id: string) { + return new Promise( ( done , fail )=> { + const timer = setTimeout( + () => fail(new Error('RPC call timeout', { cause: id })), + this.timeout() + ) + + const handle = ( event : { data: unknown } )=> { + const data = event.data as { id?: string, error?: string, result?: string } + if (! data || typeof data !== 'object') return + if( data.id !== id ) return + + clearTimeout(timer) + + this.$.$mol_dom_context.removeEventListener( 'message' , handle ) + + if( data.error ) fail( new Error( data.error ) ) + else done( data.result ) + } + + this.$.$mol_dom_context.addEventListener( 'message' , handle ) + } ) + } + + @ $mol_action + post_message(request: { id: string, name: string, args: unknown[] }) { + return null + } + + @ $mol_action + call( request : { name : string , args : unknown[] } ) { + const id = this.create_id() + this.post_message({ ...request, id }) + return $mol_wire_sync(this).call_async(id) + } + + timeout() { return 10000 } + + @ $mol_mem + proxy() { + return new Proxy( {} as Handlers, { + get : ( target : unknown , name : string )=> { + return ( ... args : unknown[] )=> this.call({ name , args }) + } + } ) + } + + + } +} diff --git a/rpc/client/frame/frame.ts b/rpc/client/frame/frame.ts index 5135e014e78..dc9905cf077 100644 --- a/rpc/client/frame/frame.ts +++ b/rpc/client/frame/frame.ts @@ -1,59 +1,37 @@ namespace $ { - export class $mol_rpc_client_frame< Handlers > extends $mol_object { + export class $mol_rpc_client_frame< Handlers extends {}> extends $mol_rpc_client { @ $mol_mem_key - static item< Handlers >( uri : string ) { - return this.make({ uri : $mol_const( uri ) }) as $mol_rpc_client_frame< Handlers > + static item< Handlers extends {}>( uri : string ): $mol_rpc_client_frame< Handlers > { + return this.make({ uri : $mol_const( uri ) }) } uri() { return '' } - @ $mol_mem - frame() { - return $mol_fiber_sync( ()=> new Promise< HTMLIFrameElement >( ( done , fail )=> { + frame_async() { + return new Promise< HTMLIFrameElement >( ( done , fail )=> { const frame = this.$.$mol_dom_context.document.createElement( 'iframe' ) frame.src = this.uri() - frame.onload = $mol_fiber_root( event => done( frame ) ) + frame.onload = () => done( frame ) frame.style.display = 'none' this.$.$mol_dom_context.document.documentElement.appendChild( frame ) - } ) ) () + } ) } - @ $mol_action - call( { name , args } : { name : string , args : any[] } ) { - - const frame = this.frame() - return $mol_fiber_sync( ()=> new Promise( ( done , fail )=> { - - const id = `$mol_rpc_client_frame:${ Date.now().toString(16) }` - - frame.contentWindow!.postMessage( { id , name , args } , '*' ) - - const handle = $mol_fiber_root( ( event : MessageEvent )=> { - if( event.data.id !== id ) return - - this.$.$mol_dom_context.removeEventListener( 'message' , handle ) - - if( event.data.error ) fail( new Error( event.data.error ) ) - else done( event.data.result ) - } ) - - this.$.$mol_dom_context.addEventListener( 'message' , handle ) - - } ) ) () - + @ $mol_mem + protected frame() { + $mol_wire_solid() + this.uri() + return $mol_wire_sync(this).frame_async() } - @ $mol_mem - proxy() { - return new Proxy( {} , { - get : ( target : any , name : string )=> { - return ( ... args : any[] )=> this.call({ name , args }) - } - } ) + @ $mol_action + override post_message(request: { id: string, name: string, args: unknown[] }) { + this.frame().contentWindow!.postMessage( request , '*' ) + return null } } diff --git a/rpc/server/iframe/iframe.ts b/rpc/server/iframe/iframe.ts new file mode 100644 index 00000000000..f6ad8481647 --- /dev/null +++ b/rpc/server/iframe/iframe.ts @@ -0,0 +1,11 @@ +namespace $ { + + export class $mol_rpc_server_iframe extends $mol_rpc_server { + protected override message(event: MessageEvent) { $mol_wire_async(this).send(event) } + send( event: MessageEvent ) { + return this.$.$mol_dom_context.parent.postMessage( this.response(event), '*' ) ?? null + } + + } + +} diff --git a/rpc/server/server.ts b/rpc/server/server.ts index 0926dee3be1..8c23f1e4994 100644 --- a/rpc/server/server.ts +++ b/rpc/server/server.ts @@ -1,34 +1,43 @@ namespace $ { - export class $mol_rpc_server extends $mol_object { - + export class $mol_rpc_server extends $mol_object { @ $mol_mem listener() { - return new this.$.$mol_dom_listener( + return new this.$.$mol_dom_listener( this.$.$mol_dom_context , 'message' , - event => $mol_wire_async(this).handle(event.data), + event => this.request(event) ? this.message(event) : null ) } + protected message(event: Message) { } + + handlers(): Record unknown> { + return {} + } + + request(event: Message) { + return event.data && typeof event.data === 'object' && 'id' in event.data + ? event.data as { id : string , name : string , args : readonly unknown[] } + : null + } + @ $mol_action - handle( { id , name , args } : { id : string , name : string , args : any[] } ) { + response(event: Message) { + const { id, name, args } = this.request(event)! - const handler = this.handlers()[ name ] || this.handlers()[ '' ] - if( !handler ) return + const response = { id, result: undefined as unknown, error: undefined as Error | undefined } try { - const result = handler( ... args ) - this.$.$mol_dom_context.parent.postMessage( { id , result } , '*' ) - } catch( error: any ) { - if( error instanceof Promise ) $mol_fail_hidden( error ) - this.$.$mol_dom_context.parent.postMessage( { id , error : error.message } , '*' ) - } + const handler = this.handlers()[ name ] || this.handlers()[ '' ] - } + if( ! handler ) throw new Error('No handler ' + name) + response.result = handler( ... args ) + } catch( err ) { + if ( $mol_fail_catch(err) ) response.error = err + } - handlers(): Record { - return {} + return response } } diff --git a/rpc/server/worker/worker.ts b/rpc/server/worker/worker.ts new file mode 100644 index 00000000000..ac76beee864 --- /dev/null +++ b/rpc/server/worker/worker.ts @@ -0,0 +1,14 @@ +namespace $ { + + export class $mol_rpc_server_worker extends $mol_rpc_server { + protected override message(event: ExtendableMessageEvent) { + event.waitUntil($mol_wire_async(this).send(event)) + } + + send( event: ExtendableMessageEvent ) { + return event.source!.postMessage( this.response(event) ) ?? null + } + + } + +} diff --git a/service/channel/channel.ts b/service/channel/channel.ts deleted file mode 100644 index 985b03db925..00000000000 --- a/service/channel/channel.ts +++ /dev/null @@ -1,49 +0,0 @@ -namespace $ { - export class $mol_service_channel extends $mol_object { - protected channel = new MessageChannel() - - timeout() { return 20000 } - - out() { return this.channel.port2 } - - result() { return $mol_wire_sync(this).result_async() } - - result_async() { - return new Promise((resolve, reject) => { - const channel = this.channel - - const handler = setTimeout( - () => reject(new Error('Channel timeout', { cause: channel })), - this.timeout() - ) - - this.destructor = () => { - clearTimeout(handler) - reject(new Error('Channel cancelled')) - } - - channel.port1.onmessage = event => { - clearTimeout(handler) - const data = event.data - const result = data?.result - const error = data?.error - - if (result) { - if (error) console.warn('Message result+error:', error) - resolve(result) - return - } - - if (! error) return resolve(result ?? null) - - reject(new Error(error, { cause: event })) - } - - channel.port1.onmessageerror = event => { - clearTimeout(handler) - reject(new Error('Message fatal error: ' + event.data, { cause: event })) - } - }) - } - } -} diff --git a/service/plugin/plugin.ts b/service/plugin/plugin.ts index f25bb8d3f26..4b85e09619a 100644 --- a/service/plugin/plugin.ts +++ b/service/plugin/plugin.ts @@ -20,7 +20,6 @@ namespace $ { static install() { } static activate() { } - static data(data: {}) { return null as null | unknown } } export class $mol_service_plugin_cache extends $mol_service_plugin_base { diff --git a/service/self/self.web.ts b/service/self/self.web.ts index cd9d94d3413..c45b695307f 100644 --- a/service/self/self.web.ts +++ b/service/self/self.web.ts @@ -12,10 +12,10 @@ namespace $ { static override init() { try { this.scope() + this.rpc_server().listener() + this.rpc_server().handlers() } catch (error) { - if ($mol_fail_catch(error)) { - console.error(error) - } + $mol_fail_log( error ) } } @@ -26,7 +26,6 @@ namespace $ { const handlers = { install: this.handle(this.install.bind(this)), activate: this.handle(this.activate.bind(this)), - message: this.handle(this.message.bind(this)), messageerror: this.message_error.bind(this), @@ -86,37 +85,30 @@ namespace $ { } protected static message_error(event: MessageEvent) { - const message = 'Message deserialization failed' - - this.$.$mol_log3_fail({ place: `${this}.message_error()`, message }) - - const port = event.ports[0] - port?.postMessage({ result: null, error: message }) + this.$.$mol_log3_fail({ + place: `${this}.message_error()`, + message: 'Deserialization failed', + data: event.data + }) } - protected static message(event: ExtendableMessageEvent) { - const data = event.data as string | null | { [k: string]: unknown } - const port = event.ports[0] + @ $mol_mem + static rpc_server() { + return this.$.$mol_rpc_server_worker.make({ + handlers: () => this.handlers() + }) + } - if ( ! data || typeof data !== 'object' ) { - const error = data ? 'Message data empty' : 'Message data is not object' - return port?.postMessage({ error, result: null }) - } + @ $mol_mem + static handlers() { + return new Proxy({}, { + get: (t, name) => { + const [klass, method] = (name as string).split('.') ?? ['', ''] + const plugin = this.$.$mol_service_plugin[klass as keyof typeof $mol_service_plugin] - for (const plugin of this.plugins()) { - try { - const result = plugin.data(data) - if (! result) continue - port?.postMessage({ error: null, result }) - return - } catch (error) { - if ( ! $mol_fail_catch(error) ) continue - this.plugin_error(error, `${plugin}.data()`) - port?.postMessage({ error: error.toString(), result: null }) - return + return (...args: unknown[] ) => (plugin as any)[method as keyof typeof plugin](...args) } - } - + }) } protected static plugin_error(error: unknown, place: string) { diff --git a/service/worker/worker.ts b/service/worker/worker.ts index e681dd1ee9d..3ea5231d88d 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -6,16 +6,40 @@ namespace $ { static path() { return 'web.js' } static send_timeout() { return 20000 } - static send(data: {}) { return null as unknown } + + protected static post_message(data: {}) { + } + + @ $mol_mem + protected static rpc() { + $mol_wire_solid() + + const rpc = this.$.$mol_rpc_client.make>({ + timeout: () => this.send_timeout(), + post_message: request => this.post_message(request) ?? null + }) + + return new Proxy({} as typeof $mol_service_plugin, { + get: (t, klass) => { + return new Proxy({} as any, { + get: (t, method) => { + return (...args: unknown[]) => rpc.call({ + name: `${klass as string}.${method as string}`, + args + }) + } + }) + } + }) + } @ $mol_mem static init() { try { this.state() + // this.rpc() } catch (error) { - if (! $mol_promise_like(error)) { - console.error(error) - } + $mol_fail_log( error ) } } diff --git a/service/worker/worker.web.ts b/service/worker/worker.web.ts index 12fe2b3d747..29e88955606 100644 --- a/service/worker/worker.web.ts +++ b/service/worker/worker.web.ts @@ -68,21 +68,11 @@ namespace $ { static override state(reset?: null) { return this.worker().state } static ready_async() { return this.container().ready } - protected static ready() { return $mol_wire_sync( this ).ready_async() } - @ $mol_action - protected static post_message(data: unknown) { - const channel = this.$.$mol_service_channel.make({ - timeout: () => this.send_timeout() - }) - - this.ready().active?.postMessage(data, [ channel.out() ]) - - return channel + protected static override post_message(data: {}) { + $mol_wire_sync( this ).ready_async().active!.postMessage(data) } - static send(data: {}) { return this.post_message(data).result() } - } $.$mol_service_worker = $mol_service_worker_web From 7849b8af2230faf0e79e4c65e740ea80bdda4998 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Thu, 7 Nov 2024 01:38:52 +0300 Subject: [PATCH 59/60] $mol_notify fixes --- notify/notify.ts | 10 +--------- service/worker/worker.ts | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/notify/notify.ts b/notify/notify.ts index 99999e96340..555bc74d784 100644 --- a/notify/notify.ts +++ b/notify/notify.ts @@ -14,20 +14,12 @@ namespace $ { } static show( info: $mol_notify_info ) { - this.$.$mol_service_worker.send(info) + this.$.$mol_service_worker.rpc().$mol_notify_service.show(info) } } export class $mol_notify_service extends $mol_service_plugin_notify { - static override data(data: {}) { - if ('uri' in data && 'message' in data) { - this.show(data as $mol_notify_info) - return true - } - return null - } - static show(info: $mol_notify_info) {} } diff --git a/service/worker/worker.ts b/service/worker/worker.ts index 3ea5231d88d..3ea292ea698 100644 --- a/service/worker/worker.ts +++ b/service/worker/worker.ts @@ -11,7 +11,7 @@ namespace $ { } @ $mol_mem - protected static rpc() { + static rpc() { $mol_wire_solid() const rpc = this.$.$mol_rpc_client.make>({ From 854046d5b856f47d0b9b67739264a3b2a9278153 Mon Sep 17 00:00:00 2001 From: Stefan Zerkalica Date: Thu, 7 Nov 2024 14:43:03 +0300 Subject: [PATCH 60/60] $mol_rpc_client fix call bug --- rpc/client/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpc/client/client.ts b/rpc/client/client.ts index 3d63b852909..af03bdb02fa 100644 --- a/rpc/client/client.ts +++ b/rpc/client/client.ts @@ -8,7 +8,7 @@ namespace $ { call_async(id: string) { return new Promise( ( done , fail )=> { const timer = setTimeout( - () => fail(new Error('RPC call timeout', { cause: id })), + () => handle({ data: { id, error: 'RPC call timeout' } }), this.timeout() )