@things-factory/shell 10.1.246 → 10.1.254

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/_index.html CHANGED
@@ -5,7 +5,9 @@
5
5
  <meta name="google" content="notranslate" />
6
6
  <title>Things Factory App</title>
7
7
  <meta name="generator" content="Things Factory Starter Kit" />
8
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
8
+ <!-- interactive-widget: on a phone the on-screen keyboard shrinks the page, so the action bar and a popup's
9
+ last field stay above it instead of under it (Chrome on Android; Safari keeps its own behaviour). -->
10
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
9
11
  <meta name="description" content="Reimagining Software" />
10
12
 
11
13
  <base href="/" />
@@ -0,0 +1,61 @@
1
+ /*
2
+ * [ Caution ]
3
+ * Since this module is being used by a service worker, be very careful about adding imports or using functions for browsers.
4
+ */
5
+
6
+ export type PushWorker = {
7
+ registration: { showNotification(title: string, options?: any): Promise<void> }
8
+ channel: { postMessage(message: any): void }
9
+ store: { add(record: any): Promise<unknown>; limit(): Promise<unknown> }
10
+ /* the title of a push that brings none */
11
+ tagline?: string
12
+ }
13
+
14
+ /**
15
+ * Shows a web push, keeps it in the worker's store and tells the open screens. The work is handed to
16
+ * `event.waitUntil` before anything is awaited - otherwise the browser may stop the worker first, and a mobile
17
+ * browser drops the notice or revokes the subscription of a site that received a push and showed nothing.
18
+ */
19
+ export function handlePush(event: { data?: any; waitUntil(work: Promise<unknown>): void }, worker: PushWorker) {
20
+ if (!event.data) {
21
+ return
22
+ }
23
+
24
+ try {
25
+ var { notification, data } = event.data.json()
26
+ } catch (e) {
27
+ notification = {
28
+ body: event.data.text()
29
+ }
30
+ }
31
+
32
+ const title = notification.title || worker.tagline
33
+
34
+ const options = {
35
+ icon: '/assets/manifest/icon-128x128.png',
36
+ badge: '/assets/manifest/badge-128x128.png',
37
+ ...notification,
38
+ data
39
+ }
40
+
41
+ event.waitUntil(
42
+ (async () => {
43
+ await worker.store.add({
44
+ timestamp: Date.now(),
45
+ ...notification,
46
+ ...data
47
+ })
48
+
49
+ await worker.store.limit()
50
+
51
+ worker.channel.postMessage({
52
+ type: 'notification',
53
+ notification,
54
+ data
55
+ })
56
+
57
+ /* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification */
58
+ await worker.registration.showNotification(title, options)
59
+ })()
60
+ )
61
+ }
@@ -6,6 +6,7 @@ import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching'
6
6
  import { StaleWhileRevalidate, NetworkFirst, CacheFirst, CacheOnly, NetworkOnly } from 'workbox-strategies'
7
7
  import 'broadcastchannel-polyfill'
8
8
  import { notificationStore } from './indexdb'
9
+ import { handlePush } from './push'
9
10
 
10
11
  const ENV = process.env
11
12
  const production = ENV['NODE-ENV'] == 'production'
@@ -114,45 +115,9 @@ if (production) {
114
115
  }
115
116
 
116
117
  /* for push notification */
117
- addEventListener('push', async event => {
118
- if (!event.data) {
119
- return
120
- }
121
-
122
- try {
123
- var { notification, data } = event.data.json()
124
- } catch (e) {
125
- notification = {
126
- body: event.data.text()
127
- }
128
- }
129
-
130
- const title = notification.title || ENV['APP-TAGLINE']
131
-
132
- const options = {
133
- icon: '/assets/manifest/icon-128x128.png',
134
- badge: '/assets/manifest/badge-128x128.png',
135
- ...notification,
136
- data
137
- }
138
-
139
- await notificationStore.add({
140
- timestamp: Date.now(),
141
- ...notification,
142
- ...data
143
- })
144
-
145
- await notificationStore.limit()
146
-
147
- channel.postMessage({
148
- type: 'notification',
149
- notification,
150
- data
151
- })
152
-
153
- /* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification */
154
- registration.showNotification(title, options)
155
- })
118
+ addEventListener('push', (event: any) =>
119
+ handlePush(event, { registration, channel, store: notificationStore, tagline: ENV['APP-TAGLINE'] })
120
+ )
156
121
 
157
122
  addEventListener('notificationclick', async event => {
158
123
  const { action, notification } = event
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/shell",
3
- "version": "10.1.246",
3
+ "version": "10.1.254",
4
4
  "description": "Core module for framework",
5
5
  "bin": {
6
6
  "things-factory": "bin/things-factory",
@@ -142,5 +142,5 @@
142
142
  "pg": "^8.7.3",
143
143
  "sqlite3": "^5.0.8"
144
144
  },
145
- "gitHead": "46c0d3c93edf765cc1f13040b1f9d03fd1d9e60b"
145
+ "gitHead": "793e2fd617db5bb917ec892e64956fb690f7e7b0"
146
146
  }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * A web push is shown inside `event.waitUntil`.
3
+ *
4
+ * ## Why this is tested
5
+ *
6
+ * Until 2026-11-05 the worker's push handler awaited its store and then called `showNotification` without handing
7
+ * any of it to `event.waitUntil`. The browser may stop a worker as soon as the handler returns, and a mobile browser
8
+ * drops such a notice or revokes the subscription of a site that received a push and showed nothing.
9
+ */
10
+ import { handlePush } from '../client/serviceworker/push'
11
+
12
+ function worker() {
13
+ const shown: Array<{ title: string; options: any }> = []
14
+ const posted: any[] = []
15
+ let release: () => void = () => {}
16
+ const showing = new Promise<void>(resolve => (release = resolve))
17
+
18
+ return {
19
+ shown,
20
+ posted,
21
+ release: () => release(),
22
+ worker: {
23
+ registration: {
24
+ showNotification: async (title: string, options: any) => {
25
+ shown.push({ title, options })
26
+ await showing
27
+ }
28
+ },
29
+ channel: { postMessage: (message: any) => void posted.push(message) },
30
+ store: { add: async () => undefined, limit: async () => undefined },
31
+ tagline: 'Operato Office'
32
+ }
33
+ }
34
+ }
35
+
36
+ function pushEvent(payload: any) {
37
+ const kept: Promise<unknown>[] = []
38
+ return {
39
+ kept,
40
+ event: {
41
+ data: { json: () => payload, text: () => JSON.stringify(payload) },
42
+ waitUntil: (work: Promise<unknown>) => void kept.push(work)
43
+ }
44
+ }
45
+ }
46
+
47
+ const PAYLOAD = {
48
+ notification: { title: '결재 차례', body: '지출결의 1건' },
49
+ data: { type: 'approval', url: 'https://office.example.com/domain/hatiolab/activity-approval/a-1', timestamp: '1' }
50
+ }
51
+
52
+ describe('a web push in the service worker', () => {
53
+ it('hands its work to waitUntil before the handler returns', () => {
54
+ const { worker: w } = worker()
55
+ const { event, kept } = pushEvent(PAYLOAD)
56
+
57
+ handlePush(event, w)
58
+
59
+ expect(kept).toHaveLength(1)
60
+ })
61
+
62
+ it('keeps the worker alive until the notice is shown, with the full address to open', async () => {
63
+ const { worker: w, shown, posted, release } = worker()
64
+ const { event, kept } = pushEvent(PAYLOAD)
65
+
66
+ handlePush(event, w)
67
+
68
+ let done = false
69
+ void kept[0].then(() => (done = true))
70
+ for (let i = 0; i < 5; i++) await Promise.resolve()
71
+
72
+ expect(shown).toEqual([{ title: '결재 차례', options: expect.objectContaining({ body: '지출결의 1건', data: PAYLOAD.data }) }])
73
+ expect(posted).toEqual([{ type: 'notification', notification: PAYLOAD.notification, data: PAYLOAD.data }])
74
+ expect(done).toBe(false)
75
+
76
+ release()
77
+ await kept[0]
78
+ expect(done).toBe(true)
79
+ })
80
+
81
+ it('takes the application tagline when the push brings no title', async () => {
82
+ const { worker: w, shown, release } = worker()
83
+ const { event, kept } = pushEvent({ notification: { body: 'b' }, data: {} })
84
+
85
+ handlePush(event, w)
86
+ release()
87
+ await kept[0]
88
+
89
+ expect(shown[0].title).toBe('Operato Office')
90
+ })
91
+ })