@things-factory/shell 10.1.247 → 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
|
-
|
|
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',
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
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": "
|
|
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
|
+
})
|