qrcode: add tally service worker (#86092)
gitea/passerelle/pipeline/head This commit looks good Details

This commit is contained in:
Corentin Sechet 2024-02-12 03:41:07 +01:00
parent 4738850fcc
commit 2842439ce1
3 changed files with 204 additions and 0 deletions

View File

@ -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',
},
),
},
)

View File

@ -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)
})
}

View File

@ -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 %}