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 }