CnOfflineQueue
The work a device has captured and not yet handed over, as a list somebody can
read. Built for the field-inspection leaf, usable by anything that queues
mutations through the offline store.
Why a list and not a count
The leaf renders a pending badge, and countPending() counts pending,
conflict and syncing.
An operation that exhausted its retries, or whose author lost the right to write
it, is failed, and failed is terminal for the replay loop. So it drops out
of that badge entirely while still sitting in the browser's IndexedDB. An
inspection somebody stood next to a citizen to record is then stranded on one
device with no reader anywhere, and nothing on any screen says so.
A queue that loses a submission is worse than one that refuses it at the door. This component is what makes the stranded work visible.
What happens to an entry that cannot be replayed
It stays. Nothing here deletes a queued operation: not on failure, not on age, not to make a number smaller. The payload the inspector captured remains exactly as captured.
It is listed, with what the server actually said, the number of attempts made, and the time it was captured. A generic "could not sync" sends somebody to a helpdesk that cannot see this device either.
It is counted apart. countStuck() is separate from countPending(),
because "12 waiting" and "12 waiting, 1 stuck" are different sentences and a
surface that adds them together says neither.
It offers Try again, which re-queues it by hand and starts the attempt count over. Never automatically: an operation the server refused five times will be refused a sixth, and a queue that retries forever is a queue that never drains. Somebody who has fixed whatever the server was objecting to asks for the retry.
Except when the right to write is gone. A permission_lost entry offers no
retry and says why. Retrying cannot restore a permission, so offering the button
would be offering a gesture that fails every time. The entry still stays and is
still listed, because somebody who does hold the right needs to see it.
And the text is recoverable. Every failed entry offers Copy what I wrote, which puts the captured payload on the clipboard. Listing a stranded capture tells somebody their work is stuck; it does not give it back to them. An inspection is text a person wrote, and when this device will never deliver it they should still be able to paste it into a mail, a form or a note rather than retyping it from memory.
The offer is made on a permission_lost entry too. They may no longer be
allowed to write it here, but they still wrote it.
Where the browser has no clipboard, which is ordinary on a field device with no
secure context, the component says so and emits copy-refused with the text, so
a host can render it for selection by hand. It never says "Copied" over an empty
clipboard: that sends somebody away believing they have their words.
What happens to a conflict
A conflicting row offers three choices, and they are three different sentences:
- Keep mine puts the operation back in the queue to be sent again, attempt count reset.
- Keep theirs ends the row and leaves this device holding the server's version. Both halves matter: marking the row done while the device still renders the abandoned local text is how somebody reads their own discarded answer back as the current record.
- Merge by hand opens the differing fields side by side, named Mine and Theirs with the actual values. Every field starts on Theirs, because a panel pre-set to the local answer puts a colleague's edit one Save away from being discarded without anybody choosing it.
Merge by hand is offered only when the server sent a version to merge against. A target deleted server-side has one version, and a button that opens an empty panel is a button that cannot do what it says.
A permission_lost row offers none of the three. No choice can re-grant a
permission, so every button would be a gesture that fails.
The choice, the uid in resolvedBy and the time are written onto the conflict
object. A record that says a collision happened and not how it was settled
cannot answer the only question anybody asks a year later.
Where the leaf's offlineConfig names no conflictSchema, the row says the
clash is recorded on this device only, so nobody waits for a colleague who will
never see it.
What happens on a device nobody opens again
Nothing, and that is the honest answer. This queue lives in one browser profile's IndexedDB and has no server-side twin. A phone that is wiped, reset or simply never opened again takes its queue with it, and no report anywhere will show it as missing, because nothing outside that device ever knew the capture existed.
The shell worker does not change this. It caches a page so the queue can be
reached with no signal; it does not hand the queue to a server. See
registerOfflineWorker in the offlineCollection reference.
That is why the surfaces above matter while the device is open, and why the stuck count now outranks everything on the leaf indicator. A consuming app that cannot accept this should drain on a schedule and treat a device silent for longer than its planning lifetime as an operational exception. This library cannot see that from inside the browser.
Usage
<template>
<CnOfflineQueue
:deviceId="deviceId"
@requeued="onRequeued"
@copy-refused="showForManualCopy" />
</template>
<script>
import { CnOfflineQueue } from '@conduction/nextcloud-vue'
import { resolveDeviceId } from '@conduction/nextcloud-vue'
export default {
components: { CnOfflineQueue },
data() {
return { deviceId: resolveDeviceId() }
},
methods: {
onRequeued(operationId) {
// The drain will pick it up on its next pass.
},
showForManualCopy({ id, text }) {
// The clipboard refused. Render `text` somewhere selectable.
},
},
}
</script>
Pass deviceId. Without it the list shows every device's queue in this browser
profile, which is only ever what a test wants.
refreshMs is how often the list re-reads itself while a drain is running, in
milliseconds. It defaults to 2000. Set it to 0 and the list stops polling,
which is what a test wants and what a device on a metered connection may want
too.
Accessibility
Every status is rendered as a word: Waiting, Sending, Needs a decision, Stuck, Sent. The colour only repeats it. This is the surface that tells somebody their morning's work is stuck, so a coloured dot alone would leave that unsaid for a reader who cannot see it (WCAG 2.2 SC 1.4.1).
OpenRegister reliance
none: this component does not touch OpenRegister, directly or through anything it imports. 0 direct references, 0 files reached through imports.
See the reliance overview for the filterable list.
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
deviceId | string | '' | Scope the list to one device. Empty lists every device's queue in this browser profile, which is only ever useful in a test. | |
resolvedBy | string | '' | The uid recorded as having settled a conflict. A conflict record that says a collision happened and not who decided it cannot answer the question it was filed for. | |
refreshMs | number | 2000 | Poll interval in ms while a drain is running. Zero disables it. |
Events
| Name | Payload | Description |
|---|---|---|
requeued | — | Emitted when a failed operation was put back in the queue by hand. Payload: the operation id. |
copy-refused | — | Emitted when the clipboard refused the capture, so a host can show the text for selection. Payload: { id, text }. |
resolved | — | Emitted when a conflict was settled from the list. Payload: { id, resolution }. |