diff --git a/notify/notify.ts b/notify/notify.ts index fe860cb306..c352c4626f 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 81a39b07b3..615e5d0f0f 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 215e984042..0000000000 --- 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 24adefdeb8..0000000000 --- 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 39e29e842c..4100598d22 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 39009d30ef..28d944c6c9 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 0000000000..d1bc9c53e9 --- /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 2a06aa7e0d..92f19584ad 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 43ba799adf..38a4b60b01 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() }