qrcode: add tally service worker (#86092)
gitea/passerelle/pipeline/head This commit looks good
Details
gitea/passerelle/pipeline/head This commit looks good
Details
This commit is contained in:
parent
4738850fcc
commit
2842439ce1
|
@ -314,6 +314,14 @@ class QRCodeConnector(BaseResource):
|
|||
'reader': reader,
|
||||
'metadata_url': reader.get_metadata_url(request),
|
||||
'tally_url': reader.get_tally_url(request),
|
||||
'service_worker_scope': reverse(
|
||||
'generic-endpoint',
|
||||
kwargs={
|
||||
'slug': self.slug,
|
||||
'connector': self.get_connector_slug(),
|
||||
'endpoint': 'open-reader',
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
const _EVENTS = 'events'
|
||||
|
||||
let db = null
|
||||
|
||||
// We could store that in an indexed db, so that the whole event list doesn't
|
||||
// get refreshed each time we relaunch the browser.
|
||||
let lastUpdateTimestamp = 0
|
||||
const tallyUrls = new Set()
|
||||
|
||||
self.addEventListener('activate', async (event) => {
|
||||
try {
|
||||
console.log('Activating QR code service worker')
|
||||
db = await openIndexedDb()
|
||||
clients.claim()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('message', async (event) => {
|
||||
try {
|
||||
const tallyUrl = event.data.refreshTally
|
||||
if (tallyUrl) {
|
||||
tallyUrls.add(tallyUrl)
|
||||
refreshTally(tallyUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
try {
|
||||
if (tallyUrls.has(event.request.url)) {
|
||||
event.respondWith(tally(event.request))
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
function openIndexedDb () {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const openDbRequest = indexedDB.open('TallyEvents', 1)
|
||||
|
||||
openDbRequest.onupgradeneeded = (event) => {
|
||||
const db = event.target.result
|
||||
const objectStore = db.createObjectStore(_EVENTS, { keyPath: 'certificate' })
|
||||
objectStore.createIndex('certificate', 'certificate', { unique: true })
|
||||
objectStore.createIndex('pending', 'pending', { unique: false })
|
||||
}
|
||||
|
||||
openDbRequest.onsuccess = (event) => {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
|
||||
openDbRequest.onerror = (error) => {
|
||||
reject(error)
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function tally (request) {
|
||||
try {
|
||||
const json = await request.json()
|
||||
const stamps = {}
|
||||
for (const tallyEvent of json.events) {
|
||||
try {
|
||||
await addEvent({ ...tallyEvent, pending: 1 })
|
||||
stamps[tallyEvent.certificate] = 'ok'
|
||||
} catch (error) {
|
||||
if (error.name === 'ConstraintError') {
|
||||
stamps[tallyEvent.certificate] = 'duplicate'
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
err: 0,
|
||||
data: {
|
||||
timestamp: lastUpdateTimestamp,
|
||||
stamps
|
||||
}
|
||||
}))
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTally (url) {
|
||||
try {
|
||||
console.info(`Refreshing tally from ${url}`)
|
||||
try {
|
||||
const tallyEvents = []
|
||||
for (const tallyEvent of await getPendingEvents()) {
|
||||
tallyEvents.push({
|
||||
certificate: tallyEvent.certificate,
|
||||
timestamp: tallyEvent.timestamp
|
||||
})
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
since: lastUpdateTimestamp,
|
||||
events: tallyEvents
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(`Error while refreshing tally : ${response.status} (${response.statusText})`)
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
const data = json.data
|
||||
|
||||
for (const certificate in data.stamps) {
|
||||
await addEvent({ certificate }, true)
|
||||
}
|
||||
|
||||
lastUpdateTimestamp = data.timestamp
|
||||
} catch (error) {
|
||||
if (error.name !== 'NetworkError') {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
function addEvent (tallyEvent, overwrite) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// It is important to create the transaction inside the promise : if
|
||||
// execution is given back to the event loop and a transaction has no
|
||||
// pending request, the transaction is closed, making following calls fail
|
||||
// with InactiveTransactionError.
|
||||
const transaction = db.transaction([_EVENTS], 'readwrite')
|
||||
const tallyEventStore = transaction.objectStore(_EVENTS)
|
||||
|
||||
// If an event with the same "certificate" key already exists, put will
|
||||
// overwrite it, and add will throw a ConstraintError, due to the
|
||||
// "certificate" field being declared as an unique index in openIndexedDB.
|
||||
const request = overwrite ? tallyEventStore.put(tallyEvent) : tallyEventStore.add(tallyEvent)
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
function getPendingEvents () {
|
||||
return new Promise((resolve, reject) => {
|
||||
// It is important to create the transaction inside the promise : if
|
||||
// execution is given back to the event loop and a transaction has no
|
||||
// pending request, the transaction is closed, making following calls fail
|
||||
// with InactiveTransactionError.
|
||||
const transaction = db.transaction([_EVENTS], 'readwrite')
|
||||
const tallyEventStore = transaction.objectStore(_EVENTS)
|
||||
|
||||
// If a stored event was received from the API, it will not have a
|
||||
// "pending" field. Even if it was added locally with a "pending" field set
|
||||
// to 1, refreshTally will overwrite it with a new event object without
|
||||
// that field.
|
||||
//
|
||||
// IndexedDB indices only return objects that have a value for the field
|
||||
// they index, so all that we have to do here is to get all events in the
|
||||
// "pending" index : they are those added locally and not yet synchronized
|
||||
// with the backend.
|
||||
const pendingIndex = tallyEventStore.index('pending')
|
||||
const request = pendingIndex.getAll()
|
||||
request.onsuccess = () => resolve(request.result)
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
|
@ -31,6 +31,20 @@
|
|||
</script>
|
||||
<link rel="stylesheet" href="{% static 'qrcode/css/style.css' %}">
|
||||
<script type="module" src="{% static 'qrcode/js/qrcode-reader.js' %}"></script>
|
||||
{% if tally_url %}
|
||||
<script type="module">
|
||||
navigator.serviceWorker.register(
|
||||
"{% static 'qrcode/js/qrcode-service-worker.js' %}", {
|
||||
scope: "/",
|
||||
})
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
function refreshTally() {
|
||||
registration.active.postMessage({refreshTally: "{{ tally_url }}"})
|
||||
}
|
||||
refreshTally()
|
||||
setInterval(refreshTally, 10000)
|
||||
</script>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
{% if not started %}
|
||||
|
|
Loading…
Reference in New Issue