@livenetworks/ashlar 1.4.1 → 1.6.0
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/README.md +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +162 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +77 -41
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +189 -99
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -55
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -0
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +14 -5
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +68 -1
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +48 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +47 -0
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +4 -1
- package/scss/config/mixins/_ln-table.scss +51 -93
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +232 -38
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +39 -0
- package/scss/ln-ashlar.scss +8 -2
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1103
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- package/js/ln-table/src/ln-table-sort.js +0 -168
- package/scss/components/_data-table.scss +0 -83
- package/scss/config/mixins/_data-table.scss +0 -346
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# `data-ln-api-queue`
|
|
2
|
+
|
|
3
|
+
A zero-dependency, Local-First **offline write outbox** component that implements the persistent-queue tier of `ln-ashlar`'s 3-tier data layer.
|
|
4
|
+
|
|
5
|
+
This component persists pending mutations to its own `IndexedDB` database and replays them, FIFO per chain, once the coordinator can execute transport for them. It is **connector-blind and id-blind** — it never calls the network itself, never knows about REST paths, and never interprets `entryId`/`targetId`/`chainKey` beyond bookkeeping. The parent `ln-data-coordinator` executes the actual transport call on the queue's `ln-api-queue:send` command and reports the outcome back via `ack` / `nack`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Declarative DOM Setup
|
|
10
|
+
|
|
11
|
+
Place the queue inside your parent coordinator element alongside your store and connector — it is an **optional** third child:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<ul data-ln-data-coordinator="documents" hidden>
|
|
15
|
+
<!-- Storage Layer Cache (Blind to networking) -->
|
|
16
|
+
<li data-ln-data-store
|
|
17
|
+
data-ln-data-store-indexes="status,updated_at">
|
|
18
|
+
</li>
|
|
19
|
+
|
|
20
|
+
<!-- Transport Gateway (REST / API Connector) -->
|
|
21
|
+
<li data-ln-api-connector
|
|
22
|
+
data-ln-api-base-url="https://api.livenetworks.com/v1"
|
|
23
|
+
data-ln-api-path="/documents">
|
|
24
|
+
</li>
|
|
25
|
+
|
|
26
|
+
<!-- Offline Outbox (optional Child 3) -->
|
|
27
|
+
<li data-ln-api-queue></li>
|
|
28
|
+
</ul>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
When this child is absent, the coordinator's write handlers call the connector directly (unchanged, queue-absent path). When present, every mutation is routed through the queue instead.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Attributes
|
|
36
|
+
|
|
37
|
+
| Attribute | Category | Description |
|
|
38
|
+
|-----------|----------|-------------|
|
|
39
|
+
| `data-ln-api-queue` | Selector | Creates the component instance. Optional value serves as a scope/name (defaults to the coordinator's scope). |
|
|
40
|
+
| `data-ln-api-queue-online` | Status (read-only, reflected) | Set by the component itself — `"true"` when the scope is draining/idle normally, `"false"` while paused (e.g. auth pause). Useful for CSS hooks on a connectivity indicator; not meant to be written by consumers. |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## IndexedDB Schema
|
|
45
|
+
|
|
46
|
+
The queue owns its **own** IndexedDB database, `ln_api_queue` — entirely separate from the store's cache database. Nothing in `ln-data-store` reads or writes this database.
|
|
47
|
+
|
|
48
|
+
| Object store | Key path | Indexes | Purpose |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `outbox` | `entryId` | `by_scope_chain` (`[scope, chainKey]`), `by_scope_seq` (`[scope, seq]`) | One row per pending mutation. `seq` is a monotonically increasing per-scope counter used to preserve global FIFO ordering within a scope; `by_scope_chain` is used to find the head entry per chain. |
|
|
51
|
+
| `_queue_meta` | `key` | — | Holds `seq:<scope>` counters and `paused:<scope>` auth-pause state. |
|
|
52
|
+
|
|
53
|
+
Each `outbox` entry carries: `entryId`, `scope`, `chainKey`, `seq`, `op`, `targetId`, `payload`, `expectedVersion`, `meta`, `attempts`, `status` (`pending` / `inflight` / `failed`), `leaseOwner`, `leaseUntil`, and timestamps. The queue never inspects `payload` — it passes it through untouched to whatever the coordinator sends on `ln-api-queue:send`.
|
|
54
|
+
|
|
55
|
+
Sequence allocation and entry insertion happen in one `readwrite` transaction
|
|
56
|
+
covering both `_queue_meta` and `outbox`. Concurrent enqueues therefore cannot
|
|
57
|
+
receive the same sequence number.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## JavaScript API Methods
|
|
62
|
+
|
|
63
|
+
Access the instance directly on the element via the `.lnApiQueue` property:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
const queueEl = document.querySelector('[data-ln-api-queue]');
|
|
67
|
+
const queue = queueEl.lnApiQueue;
|
|
68
|
+
|
|
69
|
+
// Programmatic equivalents of the DOM commands below also exist,
|
|
70
|
+
// but the canonical interface is event-driven (see next section) —
|
|
71
|
+
// this keeps the coordinator's write-routing code identical whether
|
|
72
|
+
// it is reacting to a DOM event or calling a method directly.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## DOM Events
|
|
78
|
+
|
|
79
|
+
### Commands (Dispatched TO the queue)
|
|
80
|
+
|
|
81
|
+
| Event | `detail` Payload | Effect |
|
|
82
|
+
|-------|------------------|--------|
|
|
83
|
+
| `ln-api-queue:request-enqueue` | `{ chainKey, op, targetId, payload, expectedVersion, meta }` | Assigns `entryId` + `seq`, persists the entry, emits `enqueued` + `pending-count`, then attempts a drain. |
|
|
84
|
+
| `ln-api-queue:ack` | `{ entryId }` | Deletes the entry, emits `pending-count`, advances that chain (drain). |
|
|
85
|
+
| `ln-api-queue:nack` | `{ entryId, reason }` — `reason` is `'retry'`, `'drop'`, or `'auth'` | `retry` → schedules backoff and re-sends later; `drop` → deletes the entry and advances the chain; `auth` → pauses the scope and emits `auth-required`. |
|
|
86
|
+
| `ln-api-queue:request-remap` | `{ oldKey, newId }` | For pending entries whose chain is `oldKey`: any entry with `targetId === oldKey` gets `targetId = newId`; the chain itself is re-keyed from `oldKey` to `newId`; if `meta.action` contains `oldKey` as a substring, it is string-replaced with `newId` too (keeps a persisted per-record URL in sync after a create resolves). |
|
|
87
|
+
| `ln-api-queue:resolve-create` | `{ entryId, oldKey, newId }` | Atomically deletes the acknowledged create and remaps every queued sibling from the temp key to the authoritative id in one transaction, then advances the chain. |
|
|
88
|
+
| `ln-api-queue:request-resume` | `{}` | Clears the pause on this scope and resumes draining. |
|
|
89
|
+
| `ln-api-queue:request-drain` | `{}` | Manually triggers a drain attempt (e.g. to retry `failed` entries). |
|
|
90
|
+
| `ln-api-queue:request-clear` | `{}` | Deletes all entries for this scope (e.g. on logout). |
|
|
91
|
+
|
|
92
|
+
### Notifications (Emitted BY the queue, bubble to the coordinator)
|
|
93
|
+
|
|
94
|
+
| Event | `detail` Payload | Meaning |
|
|
95
|
+
|-------|------------------|---------|
|
|
96
|
+
| `ln-api-queue:send` | `{ entryId, chainKey, op, targetId, payload, expectedVersion, idempotencyKey, meta }` | **Command to the coordinator** — execute transport for this head-of-chain entry. `idempotencyKey` is the stable `entryId`. The queue never calls `fetch` itself. |
|
|
97
|
+
| `ln-api-queue:enqueued` | `{ entryId, chainKey, count }` | An entry was persisted. |
|
|
98
|
+
| `ln-api-queue:pending-count` | `{ count, scope }` | Live outbox depth for the scope — the primary UI-facing signal (badge, banner). |
|
|
99
|
+
| `ln-api-queue:auth-required` | `{ entryId, chainKey }` | The scope paused after a `401`/`419` nack. The consumer drives re-authentication, then dispatches `request-resume`. |
|
|
100
|
+
| `ln-api-queue:paused` | `{ reason }` | Draining paused for this scope. |
|
|
101
|
+
| `ln-api-queue:resumed` | `{}` | Draining resumed for this scope. |
|
|
102
|
+
| `ln-api-queue:failed` | `{ entryId, chainKey, attempts }` | Retries exhausted (see backoff below); the entry is retained (not deleted) for a manual `request-drain`. |
|
|
103
|
+
| `ln-api-queue:drained` | `{ scope }` | The outbox for this scope is empty. |
|
|
104
|
+
| `ln-api-queue:error` | `{ operation, entryId?, error }` | IndexedDB or queue orchestration failed without silently discarding the entry. |
|
|
105
|
+
| `ln-api-queue:destroyed` | `{ scope }` | The instance was torn down. |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## FIFO-per-Chain Semantics
|
|
110
|
+
|
|
111
|
+
Entries are ordered **per `(scope, chainKey)`**, not globally. Each chain drains strictly in the order its entries were enqueued (via the `seq` counter), and the queue keeps **one inflight entry per chain** at a time — it will not dispatch `ln-api-queue:send` for the next entry in a chain until the current head is `ack`'d or `nack`'d with `'drop'`. A `failed` head blocks newer siblings until an explicit `request-drain` resets it. Different chains drain concurrently and independently of each other.
|
|
112
|
+
|
|
113
|
+
`chainKey` is typically the record id (or temp id, pre-confirmation) — this is what makes a create followed by an update on the same not-yet-confirmed record safe: the update waits behind the create in the same chain, and `request-remap` re-keys the chain once the create resolves.
|
|
114
|
+
|
|
115
|
+
Claiming is transactional. A ready head is changed from `pending` to
|
|
116
|
+
`inflight` in the same IndexedDB transaction that selects it. The claim carries
|
|
117
|
+
a 60-second lease; another queue instance or a repeated drain sees the lease
|
|
118
|
+
and cannot dispatch the same entry. If a tab crashes, the entry becomes
|
|
119
|
+
claimable again after `leaseUntil`.
|
|
120
|
+
|
|
121
|
+
Claim safety also applies when multiple tabs overlap: IndexedDB read/write
|
|
122
|
+
transactions serialize claims against the shared database, so mutual exclusion
|
|
123
|
+
does not depend on `BroadcastChannel`. `leaseOwner` identifies the queue
|
|
124
|
+
instance that owns the current claim; `leaseUntil` provides crash recovery.
|
|
125
|
+
This is not a cross-tab worker-pool contract: each instance immediately drains
|
|
126
|
+
its own enqueues, while another already-open tab may discover work only on an
|
|
127
|
+
existing drain trigger. Cross-tab messaging may be added later as a discovery
|
|
128
|
+
latency optimization, but it is not the correctness mechanism.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Drain-on-Init
|
|
133
|
+
|
|
134
|
+
On construction, the queue restores the persisted per-scope auth pause and
|
|
135
|
+
attempts to drain every unpaused chain. Pending writes from a previous page
|
|
136
|
+
load resume automatically. A persisted `inflight` entry is not duplicated
|
|
137
|
+
while its lease is active and is recovered when the lease expires.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Backoff & Retry Limits
|
|
142
|
+
|
|
143
|
+
On a `nack {reason:'retry'}` (5xx or network failure), the entry is retried with exponential backoff: **2s, 5s, 15s, 60s, 5min**. After **8 attempts** the entry's status becomes `'failed'` and `ln-api-queue:failed` fires — the entry is **retained**, not deleted, so a manual `ln-api-queue:request-drain` (or a UI-driven "retry failed" action) can attempt it again later.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Auth Pause / Resume
|
|
148
|
+
|
|
149
|
+
A `nack {reason:'auth'}` (401/419) pauses the **entire scope** — not just the one chain — and emits `ln-api-queue:auth-required`. The optimistic write is **not** reverted; it stays pending in the cache. The consumer is expected to drive re-authentication (e.g. redirect to login, refresh a token) and then dispatch `ln-api-queue:request-resume` on the queue element to clear the pause and resume draining.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Temp-ID Remap Collaboration
|
|
154
|
+
|
|
155
|
+
The queue itself has no idea what a "temp id" is — remap is purely a re-keying operation the coordinator asks for. On a successful `create`, the coordinator:
|
|
156
|
+
|
|
157
|
+
1. Reconciles the store with an ordinary correlated `request-update`.
|
|
158
|
+
2. Waits for `ln-data-store:updated` to confirm that the id rekey committed.
|
|
159
|
+
3. Dispatches one `ln-api-queue:resolve-create { entryId, oldKey: tempId, newId: serverId }`.
|
|
160
|
+
|
|
161
|
+
The compound command removes the old async remap/ack race: the acknowledged
|
|
162
|
+
create is deleted and every waiting sibling is re-keyed before the transaction
|
|
163
|
+
commits or the chain advances.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Connector- and ID-Blind by Design
|
|
168
|
+
|
|
169
|
+
The queue never imports or references a connector, never constructs a URL, and never distinguishes a temp id from a real id except as an opaque `chainKey`/`targetId` string it is told to match and re-key. All transport execution, id semantics, and REST/verb mapping live in the coordinator (`ln-data-coordinator`) and the connector it drives — see their READMEs for that half of the contract.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 3-Tier Integration Architecture (Coordinator Example)
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
(function () {
|
|
177
|
+
const parent = document.querySelector('[data-ln-data-coordinator="documents"]');
|
|
178
|
+
const queueEl = parent.querySelector('[data-ln-api-queue]');
|
|
179
|
+
const connectorEl = parent.querySelector('[data-ln-api-connector]');
|
|
180
|
+
|
|
181
|
+
if (!queueEl || !connectorEl) return;
|
|
182
|
+
|
|
183
|
+
// The queue commands the coordinator to execute transport for its head entry.
|
|
184
|
+
queueEl.addEventListener('ln-api-queue:send', function (e) {
|
|
185
|
+
const { entryId, op, targetId, payload, expectedVersion, idempotencyKey, meta } = e.detail;
|
|
186
|
+
|
|
187
|
+
const call = op === 'create' ? connectorEl.lnConnector.create(payload)
|
|
188
|
+
: op === 'update' ? connectorEl.lnConnector.update(targetId, payload, expectedVersion)
|
|
189
|
+
: op === 'delete' ? connectorEl.lnConnector.delete(targetId)
|
|
190
|
+
: connectorEl.lnConnector.bulkDelete(meta.ids);
|
|
191
|
+
|
|
192
|
+
call.then(function (record) {
|
|
193
|
+
// For create: wait for the correlated store rekey, then dispatch
|
|
194
|
+
// resolve-create. Other operations use ack.
|
|
195
|
+
const eventName = op === 'create' ? 'ln-api-queue:resolve-create' : 'ln-api-queue:ack';
|
|
196
|
+
const detail = op === 'create'
|
|
197
|
+
? { entryId, oldKey: meta.tempId, newId: record.id }
|
|
198
|
+
: { entryId };
|
|
199
|
+
queueEl.dispatchEvent(new CustomEvent(eventName, { detail }));
|
|
200
|
+
}).catch(function (err) {
|
|
201
|
+
const reason = (err.status === 401 || err.status === 419) ? 'auth'
|
|
202
|
+
: (!err.status || err.status >= 500) ? 'retry'
|
|
203
|
+
: 'drop';
|
|
204
|
+
queueEl.dispatchEvent(new CustomEvent('ln-api-queue:nack', { detail: { entryId, reason } }));
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Surface outbox depth for a UI badge.
|
|
209
|
+
queueEl.addEventListener('ln-api-queue:pending-count', function (e) {
|
|
210
|
+
document.querySelector('#pending-badge').textContent = e.detail.count;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Drive re-login on auth pause, then resume.
|
|
214
|
+
queueEl.addEventListener('ln-api-queue:auth-required', function () {
|
|
215
|
+
// redirect / refresh token, then:
|
|
216
|
+
queueEl.dispatchEvent(new CustomEvent('ln-api-queue:request-resume'));
|
|
217
|
+
});
|
|
218
|
+
})();
|
|
219
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const r=console.warn;console.warn=function(...t){typeof t[0]=="string"&&(t[0].startsWith("[ln-")||t[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||r.apply(console,t)}}const E={};function N(r,t){E[r]||(E[r]=document.querySelector('[data-ln-template="'+r+'"]'));const s=E[r];return s?s.content.cloneNode(!0):(console.warn("["+(t||"ln-core")+'] Template "'+r+'" not found'),null)}function y(r,t,s){r.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:s||{}}))}function S(r,t){if(!r||!t)return r;const s=r.querySelectorAll("[data-ln-field]");for(let o=0;o<s.length;o++){const i=s[o],d=i.getAttribute("data-ln-field");t[d]!=null&&(i.textContent=t[d])}const l=r.querySelectorAll("[data-ln-attr]");for(let o=0;o<l.length;o++){const i=l[o],d=i.getAttribute("data-ln-attr").split(",");for(let e=0;e<d.length;e++){const n=d[e].trim().split(":");if(n.length!==2)continue;const a=n[0].trim(),f=n[1].trim();t[f]!=null&&i.setAttribute(a,t[f])}}const c=r.querySelectorAll("[data-ln-show]");for(let o=0;o<c.length;o++){const i=c[o],d=i.getAttribute("data-ln-show");d in t&&i.classList.toggle("hidden",!t[d])}const u=r.querySelectorAll("[data-ln-class]");for(let o=0;o<u.length;o++){const i=u[o],d=i.getAttribute("data-ln-class").split(",");for(let e=0;e<d.length;e++){const n=d[e].trim().split(":");if(n.length!==2)continue;const a=n[0].trim(),f=n[1].trim();f in t&&i.classList.toggle(a,!!t[f])}}return r}function U(r,t){r.matches&&r.matches("[data-ln-form], [data-ln-fillable]")&&r.dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0}));const s=r.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let l=0;l<s.length;l++)s[l].dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0}));return r}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(r){if(!(!r.target.matches||!r.target.matches("[data-ln-fillable]")))if(r.detail)S(r.target,r.detail);else{const t=r.target.querySelectorAll("[data-ln-field]");for(let s=0;s<t.length;s++)t[s].textContent=""}})));function D(r,t){if(!r||!t)return r;const s=document.createTreeWalker(r,NodeFilter.SHOW_TEXT);for(;s.nextNode();){const u=s.currentNode;u.textContent.indexOf("{{")!==-1&&(u.textContent=u.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(o,i){return t[i]!==void 0?t[i]:""}))}const l=function(u,o){return t[o]!==void 0?t[o]:""},c=Array.from(r.querySelectorAll("*"));r.nodeType===1&&c.push(r);for(let u=0;u<c.length;u++){const o=c[u],i=o.attributes;for(let d=0;d<i.length;d++){const e=i[d];e.value.indexOf("{{")!==-1&&o.setAttribute(e.name,e.value.replace(/\{\{\s*(\w+)\s*\}\}/g,l))}}return r}function F(r,t,s,l,c,u){const o={};for(let d=0;d<r.children.length;d++){const e=r.children[d],n=e.getAttribute("data-ln-key");n&&(o[n]=e)}const i=document.createDocumentFragment();for(let d=0;d<t.length;d++){const e=t[d],n=String(l(e));let a=o[n];if(a)c(a,e,d);else{const f=N(s,u);if(!f||(D(f,e),a=f.firstElementChild,!a))continue;a.setAttribute("data-ln-key",n),c(a,e,d)}i.appendChild(a)}r.textContent="",r.appendChild(i)}function P(r,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){P(r,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}r()}function v(r,t,s,l){if(r.nodeType!==1)return;const u=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",o=Array.from(r.querySelectorAll(u));r.matches&&r.matches(u)&&o.push(r);for(const i of o)i[s]||(i[s]=new l(i))}function j(r,t,s,l,c={}){const u=c.extraAttributes||[],o=c.onAttributeChange||null,i=c.onInit||null;function d(e){const n=e||document.body;v(n,r,t,s),i&&i(n)}return P(function(){const e=new MutationObserver(function(a){for(let f=0;f<a.length;f++){const h=a[f];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(v(p,r,t,s),i&&i(p))}for(let m=0;m<h.removedNodes.length;m++){const p=h.removedNodes[m];if(p.nodeType===1){const A=r.indexOf("[")!==-1||r.indexOf(".")!==-1||r.indexOf("#")!==-1?r:"["+r+"]",C=Array.from(p.querySelectorAll(A));p.matches&&p.matches(A)&&C.push(p);for(let I=0;I<C.length;I++){const M=C[I];if(!document.contains(M)){const L=M[t];L&&typeof L.destroy=="function"&&L.destroy()}}}}}else h.type==="attributes"&&(o&&h.target[t]?o(h.target,h.attributeName):(v(h.target,r,t,s),i&&i(h.target)))}});let n=[];if(r.indexOf("[")!==-1){const a=/\[([\w-]+)/g;let f;for(;(f=a.exec(r))!==null;)n.push(f[1])}else n.push(r);e.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:n.concat(u)})},l),window[t]=d,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){d(document.body)}):d(document.body),d}const T={};function W(r,t){T[r]=t}function V(r){return T[r]||{ingress:t=>t,egress:t=>t}}const Q={};function O(r,t){if(!r||typeof t!="object")return;const s=r.toLowerCase().split("-")[0];Q[s]=t}function H(r){if(!r)return null;const t=r.toLowerCase().split("-")[0];return Q[t]||null}O("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=W,window.lnCore.getDataMapper=V,window.lnCore.registerLocaleFallback=O,window.lnCore.getLocaleFallback=H,window.lnCore.fillTemplate=D,window.lnCore.fill=S,window.lnCore.lnFill=U,window.lnCore.renderList=F);function R(r){return(r||"").replace(/^#/,"")}function k(r){const t=r===void 0?location.hash:r,s={},l=R(t);if(!l)return s;const c=l.split("&");for(let u=0;u<c.length;u++){const o=c[u];if(!o)continue;const i=o.indexOf(":"),d=i>-1?o.slice(0,i):o,e=i>-1?o.slice(i+1):"";if(d)try{s[d]=decodeURIComponent(e)}catch{s[d]=e}}return s}function X(r){if(!r)return null;const t=k();return r in t?t[r]:null}function z(r,t){if(!r)return;const s=k();t==null?delete s[r]:s[r]=String(t);const c=Object.keys(s).map(function(u){const o=s[u];return o===""?u:u+":"+encodeURIComponent(o)}).join("&");R(location.hash)!==c&&(location.hash=c)}function G(r){return r.button===1||r.ctrlKey||r.metaKey||r.shiftKey?!1:(r.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=k,window.lnCore.hashGet=X,window.lnCore.hashSet=z,window.lnCore.hashLinkClick=G);const J="ln_api_queue",Y=2,_="outbox",w="_queue_meta";function g(r,t){return r.error||new Error(t)}function b(r,t){return r.bound([t,-1/0],[t,1/0])}function K(r){return"seq:"+r}function q(r){return"paused:"+r}function B(r){r.leaseOwner=null,r.leaseUntil=0}function Z(r,t,s){return typeof r!="string"||r.indexOf(t)===-1?r:r.split(t).join(s)}function $(r,t,s,l){const c=new Map,u=[],o=[];for(const i of r||[])c.has(i.chainKey)||c.set(i.chainKey,[]),c.get(i.chainKey).push(i);return c.forEach((i,d)=>{i.sort((n,a)=>n.seq-a.seq);const e=i[0];if(!(!e||e.status==="failed")){if(e.status==="inflight"&&(e.leaseUntil||0)>l){o.push({chainKey:d,at:e.leaseUntil});return}if((e.nextAttemptAt||0)>l){o.push({chainKey:d,at:e.nextAttemptAt});return}e.status="inflight",e.leaseOwner=t,e.leaseUntil=l+s,e.updatedAt=l,u.push(e)}}),{entries:u,wakeups:o}}function ee(r,t,s,l,c){const u=[],o=[];for(const i of r||[]){if(i.entryId===t){o.push(i.entryId);continue}i.chainKey===s&&(i.chainKey=l,i.targetId===s&&(i.targetId=l),i.meta&&i.meta.id===s&&(i.meta.id=l),i.meta&&typeof i.meta.action=="string"&&(i.meta.action=Z(i.meta.action,s,l)),i.updatedAt=c,u.push(i))}return{changed:u,deleted:o}}class te{constructor(t){t=t||{},this.indexedDB=t.indexedDB||globalThis.indexedDB,this.keyRange=t.IDBKeyRange||globalThis.IDBKeyRange,this.dbName=t.dbName||J,this.now=t.now||(()=>Date.now()),this.uuid=t.uuid||(()=>crypto.randomUUID()),this._db=null,this._ready=null}open(){return this._ready?this._ready:!this.indexedDB||!this.keyRange?Promise.resolve(null):(this._ready=new Promise((t,s)=>{const l=this.indexedDB.open(this.dbName,Y);l.onupgradeneeded=c=>{const u=c.target.result;let o;u.objectStoreNames.contains(_)?o=c.target.transaction.objectStore(_):o=u.createObjectStore(_,{keyPath:"entryId"}),o.indexNames.contains("by_scope_chain")||o.createIndex("by_scope_chain",["scope","chainKey"],{unique:!1}),o.indexNames.contains("by_scope_seq")||o.createIndex("by_scope_seq",["scope","seq"],{unique:!1}),u.objectStoreNames.contains(w)||u.createObjectStore(w,{keyPath:"key"})},l.onerror=()=>s(g(l,"Queue database open failed")),l.onsuccess=c=>{this._db=c.target.result,this._db.onversionchange=()=>this.close(),t(this._db)}}),this._ready)}close(){this._db&&this._db.close(),this._db=null,this._ready=null}deleteDatabase(){return this.close(),this.indexedDB?new Promise((t,s)=>{const l=this.indexedDB.deleteDatabase(this.dbName);l.onsuccess=()=>t(),l.onerror=()=>s(g(l,"Queue database delete failed")),l.onblocked=()=>s(new Error("Queue database delete blocked"))}):Promise.resolve()}allForScope(t){return this.open().then(s=>s?new Promise((l,c)=>{const o=s.transaction(_,"readonly").objectStore(_).index("by_scope_seq").getAll(b(this.keyRange,t));o.onsuccess=()=>l(o.result||[]),o.onerror=()=>c(g(o,"Queue scope read failed"))}):[])}enqueue(t,s){return s=s||{},this.open().then(l=>l?new Promise((c,u)=>{const o=l.transaction([w,_],"readwrite"),i=o.objectStore(w),d=o.objectStore(_),e=K(t);let n=null;const a=h=>{const m=h+1;n={entryId:this.uuid(),scope:t,chainKey:s.chainKey,seq:m,op:s.op,targetId:s.targetId!==void 0?s.targetId:null,payload:s.payload,expectedVersion:s.expectedVersion!==void 0?s.expectedVersion:null,meta:s.meta||{},attempts:0,nextAttemptAt:0,status:"pending",leaseOwner:null,leaseUntil:0,createdAt:this.now(),updatedAt:this.now()},i.put({key:e,value:m}),d.put(n)},f=i.get(e);f.onerror=()=>u(g(f,"Queue sequence read failed")),f.onsuccess=()=>{const h=f.result;if(h&&typeof h.value=="number"){a(h.value);return}const m=d.index("by_scope_seq").getAll(b(this.keyRange,t));m.onerror=()=>u(g(m,"Queue sequence migration failed")),m.onsuccess=()=>{const p=(m.result||[]).reduce((x,A)=>Math.max(x,A.seq||0),0);a(p)}},o.oncomplete=()=>c(n),o.onerror=()=>u(o.error||new Error("Queue enqueue transaction failed")),o.onabort=()=>u(o.error||new Error("Queue enqueue transaction aborted"))}):null)}claimReady(t,s,l){return this.open().then(c=>c?new Promise((u,o)=>{const i=c.transaction(_,"readwrite"),d=i.objectStore(_),e=d.index("by_scope_seq").getAll(b(this.keyRange,t)),n=this.now();let a={entries:[],wakeups:[]};e.onerror=()=>o(g(e,"Queue claim read failed")),e.onsuccess=()=>{a=$(e.result||[],s,l,n);for(const f of a.entries)d.put(f)},i.oncomplete=()=>u(a),i.onerror=()=>o(i.error||new Error("Queue claim transaction failed")),i.onabort=()=>o(i.error||new Error("Queue claim transaction aborted"))}):{entries:[],wakeups:[]})}ack(t,s){return this._updateEntry(t,s,(l,c)=>(c.delete(l.entryId),{status:"acked",entry:l}))}nack(t,s,l,c){c=c||{};const u=c.maxAttempts||8,o=c.backoff||[2e3,5e3,15e3,6e4,3e5];return this.open().then(i=>i?new Promise((d,e)=>{const n=i.transaction([_,w],"readwrite"),a=n.objectStore(_),f=n.objectStore(w),h=a.get(s);let m=null;h.onerror=()=>e(g(h,"Queue nack read failed")),h.onsuccess=()=>{const p=h.result;if(!(!p||p.scope!==t)){if(l==="drop"){a.delete(p.entryId),m={status:"dropped",entry:p};return}if(B(p),p.updatedAt=this.now(),l==="auth"){p.status="pending",a.put(p),f.put({key:q(t),value:!0}),m={status:"auth",entry:p};return}if(l==="retry"){if(p.attempts=(p.attempts||0)+1,p.attempts>=u){p.status="failed",p.nextAttemptAt=0,a.put(p),m={status:"failed",entry:p};return}const x=o[Math.min(p.attempts-1,o.length-1)];p.status="pending",p.nextAttemptAt=this.now()+x,a.put(p),m={status:"retry",entry:p,delay:x}}}},n.oncomplete=()=>d(m),n.onerror=()=>e(n.error||new Error("Queue nack transaction failed")),n.onabort=()=>e(n.error||new Error("Queue nack transaction aborted"))}):null)}remap(t,s,l){return this._remapTransaction(t,null,s,l)}resolveCreate(t,s,l,c){return this._remapTransaction(t,s,l,c)}_remapTransaction(t,s,l,c){return this.open().then(u=>u?new Promise((o,i)=>{const d=u.transaction(_,"readwrite"),e=d.objectStore(_),n=e.index("by_scope_seq").getAll(b(this.keyRange,t));let a={changed:[],deleted:[]};n.onerror=()=>i(g(n,"Queue remap read failed")),n.onsuccess=()=>{a=ee(n.result||[],s,l,c,this.now());for(const f of a.deleted)e.delete(f);for(const f of a.changed)e.put(f)},d.oncomplete=()=>o(a.changed),d.onerror=()=>i(d.error||new Error("Queue remap transaction failed")),d.onabort=()=>i(d.error||new Error("Queue remap transaction aborted"))}):[])}resetFailed(t){return this.open().then(s=>s?new Promise((l,c)=>{const u=s.transaction(_,"readwrite"),o=u.objectStore(_),i=o.index("by_scope_seq").getAll(b(this.keyRange,t));let d=0;i.onerror=()=>c(g(i,"Queue failed-entry read failed")),i.onsuccess=()=>{for(const e of i.result||[])e.status==="failed"&&(e.status="pending",e.attempts=0,e.nextAttemptAt=0,e.updatedAt=this.now(),B(e),o.put(e),d++)},u.oncomplete=()=>l(d),u.onerror=()=>c(u.error||new Error("Queue failed-entry reset failed")),u.onabort=()=>c(u.error||new Error("Queue failed-entry reset aborted"))}):0)}getPaused(t){return this.open().then(s=>s?new Promise((l,c)=>{const o=s.transaction(w,"readonly").objectStore(w).get(q(t));o.onsuccess=()=>l(!!(o.result&&o.result.value)),o.onerror=()=>c(g(o,"Queue pause-state read failed"))}):!1)}setPaused(t,s){return this.open().then(l=>{if(l)return new Promise((c,u)=>{const o=l.transaction(w,"readwrite");o.objectStore(w).put({key:q(t),value:!!s}),o.oncomplete=()=>c(),o.onerror=()=>u(o.error||new Error("Queue pause-state write failed")),o.onabort=()=>u(o.error||new Error("Queue pause-state write aborted"))})})}clear(t){return this.open().then(s=>{if(s)return new Promise((l,c)=>{const u=s.transaction([_,w],"readwrite"),i=u.objectStore(_).index("by_scope_seq").openCursor(b(this.keyRange,t));i.onsuccess=d=>{const e=d.target.result;e&&(e.delete(),e.continue())},i.onerror=()=>c(g(i,"Queue clear failed")),u.objectStore(w).delete(K(t)),u.objectStore(w).delete(q(t)),u.oncomplete=()=>l(),u.onerror=()=>c(u.error||new Error("Queue clear transaction failed")),u.onabort=()=>c(u.error||new Error("Queue clear transaction aborted"))})})}_updateEntry(t,s,l){return this.open().then(c=>c?new Promise((u,o)=>{const i=c.transaction(_,"readwrite"),d=i.objectStore(_),e=d.get(s);let n=null;e.onerror=()=>o(g(e,"Queue entry read failed")),e.onsuccess=()=>{const a=e.result;!a||a.scope!==t||(n=l(a,d))},i.oncomplete=()=>u(n),i.onerror=()=>o(i.error||new Error("Queue entry transaction failed")),i.onabort=()=>o(i.error||new Error("Queue entry transaction aborted"))}):null)}}(function(){const r="data-ln-api-queue",t="lnApiQueue",s=[2e3,5e3,15e3,6e4,3e5],l=8,c=6e4;if(window[t]!==void 0)return;function u(){try{return crypto.randomUUID()}catch{return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{const a=Math.random()*16|0;return(n==="x"?a:a&3|8).toString(16)})}}const o=new te({indexedDB:window.indexedDB,IDBKeyRange:window.IDBKeyRange,uuid:u});function i(e){this.dom=e,e[t]=this;const n=e.closest("[data-ln-data-coordinator]");this.scope=e.getAttribute(r)||(n?n.getAttribute("data-ln-data-coordinator"):null)||"default",this._paused=!1,this._timers=new Map,this._workerId=u(),this._drainPromise=null,this._onlineHandler=()=>this._drain(),this._bindEvents(),window.addEventListener("online",this._onlineHandler);const a=this;return o.open().then(f=>f?o.getPaused(a.scope):(console.warn("[ln-api-queue] IndexedDB not available — queue disabled"),!1)).then(f=>(a._paused=!!f,a._paused&&y(a.dom,"ln-api-queue:paused",{reason:"auth",restored:!0}),a._emitPendingCount())).then(()=>a._drain()).catch(f=>{console.error("[ln-api-queue] Initialization failed:",f),y(a.dom,"ln-api-queue:error",{operation:"initialize",error:f})}),this}i.prototype._isOnline=function(){const e=this.dom.getAttribute("data-ln-api-queue-online");return e==="true"?!0:e==="false"?!1:navigator.onLine},i.prototype._emitPendingCount=function(){const e=this;return o.allForScope(e.scope).then(n=>(y(e.dom,"ln-api-queue:pending-count",{count:n.length,scope:e.scope}),n.length===0&&y(e.dom,"ln-api-queue:drained",{scope:e.scope}),n))},i.prototype._clearTimer=function(e){const n=this._timers.get(e);n&&(clearTimeout(n),this._timers.delete(e))},i.prototype._scheduleTimer=function(e,n){const a=Math.max(0,n),f=this._timers.get(e);f&&clearTimeout(f);const h=this,m=setTimeout(()=>{h._timers.delete(e),h._drain()},a);this._timers.set(e,m)},i.prototype._drain=function(){const e=this;return e._paused||!e._isOnline()?Promise.resolve():(e._drainPromise||(e._drainPromise=o.claimReady(e.scope,e._workerId,c).then(n=>{for(const a of n.wakeups)e._scheduleTimer(a.chainKey,a.at-Date.now());for(const a of n.entries)e._clearTimer(a.chainKey),y(e.dom,"ln-api-queue:send",{entryId:a.entryId,chainKey:a.chainKey,op:a.op,targetId:a.targetId,payload:a.payload,expectedVersion:a.expectedVersion,idempotencyKey:a.entryId,meta:a.meta})}).catch(n=>{console.error("[ln-api-queue] Drain failed:",n),y(e.dom,"ln-api-queue:error",{operation:"drain",error:n})}).finally(()=>{e._drainPromise=null})),e._drainPromise)},i.prototype._onEnqueue=function(e){const n=this;return o.enqueue(n.scope,e.detail||{}).then(a=>{if(a)return n._emitPendingCount().then(f=>(y(n.dom,"ln-api-queue:enqueued",{entryId:a.entryId,chainKey:a.chainKey,count:f.length}),n._drain()))}).catch(a=>{y(n.dom,"ln-api-queue:error",{operation:"enqueue",error:a})})},i.prototype._onAck=function(e){const n=this,a=e.detail||{};return o.ack(n.scope,a.entryId).then(()=>n._emitPendingCount()).then(()=>n._drain()).catch(f=>{y(n.dom,"ln-api-queue:error",{operation:"ack",entryId:a.entryId,error:f})})},i.prototype._onNack=function(e){const n=this,a=e.detail||{};return o.nack(n.scope,a.entryId,a.reason,{maxAttempts:l,backoff:s}).then(f=>{if(f)return f.status==="failed"?y(n.dom,"ln-api-queue:failed",{entryId:f.entry.entryId,chainKey:f.entry.chainKey,attempts:f.entry.attempts}):f.status==="retry"?n._scheduleTimer(f.entry.chainKey,f.delay):f.status==="auth"&&(n._paused=!0,y(n.dom,"ln-api-queue:paused",{reason:"auth"}),y(n.dom,"ln-api-queue:auth-required",{entryId:f.entry.entryId,chainKey:f.entry.chainKey})),n._emitPendingCount().then(()=>{if(f.status==="dropped")return n._drain()})}).catch(f=>{y(n.dom,"ln-api-queue:error",{operation:"nack",entryId:a.entryId,error:f})})},i.prototype._onRemap=function(e){const n=this,a=e.detail||{};return o.remap(n.scope,a.oldKey,a.newId).catch(f=>{y(n.dom,"ln-api-queue:error",{operation:"remap",error:f})})},i.prototype._onResolveCreate=function(e){const n=this,a=e.detail||{};return o.resolveCreate(n.scope,a.entryId,a.oldKey,a.newId).then(()=>n._emitPendingCount()).then(()=>n._drain()).catch(f=>{y(n.dom,"ln-api-queue:error",{operation:"resolve-create",entryId:a.entryId,error:f})})},i.prototype._onResume=function(){const e=this;return o.setPaused(e.scope,!1).then(()=>(e._paused=!1,y(e.dom,"ln-api-queue:resumed",{}),e._drain())).catch(n=>{y(e.dom,"ln-api-queue:error",{operation:"resume",error:n})})},i.prototype._onDrain=function(){const e=this;return o.resetFailed(e.scope).then(()=>{const n=e._drainPromise;return n?n.then(()=>e._drain()):e._drain()}).catch(n=>{y(e.dom,"ln-api-queue:error",{operation:"manual-drain",error:n})})},i.prototype._onClear=function(){const e=this;return e._timers.forEach(n=>clearTimeout(n)),e._timers.clear(),o.clear(e.scope).then(()=>{e._paused=!1,y(e.dom,"ln-api-queue:pending-count",{count:0,scope:e.scope}),y(e.dom,"ln-api-queue:drained",{scope:e.scope})}).catch(n=>{y(e.dom,"ln-api-queue:error",{operation:"clear",error:n})})},i.prototype._bindEvents=function(){const e=this;e._handlers={enqueue:n=>e._onEnqueue(n),ack:n=>e._onAck(n),nack:n=>e._onNack(n),remap:n=>e._onRemap(n),resolveCreate:n=>e._onResolveCreate(n),resume:()=>e._onResume(),drain:()=>e._onDrain(),clear:()=>e._onClear()},e.dom.addEventListener("ln-api-queue:request-enqueue",e._handlers.enqueue),e.dom.addEventListener("ln-api-queue:ack",e._handlers.ack),e.dom.addEventListener("ln-api-queue:nack",e._handlers.nack),e.dom.addEventListener("ln-api-queue:request-remap",e._handlers.remap),e.dom.addEventListener("ln-api-queue:resolve-create",e._handlers.resolveCreate),e.dom.addEventListener("ln-api-queue:request-resume",e._handlers.resume),e.dom.addEventListener("ln-api-queue:request-drain",e._handlers.drain),e.dom.addEventListener("ln-api-queue:request-clear",e._handlers.clear)},i.prototype.destroy=function(){if(!this.dom[t])return;const e=this;e.dom.removeEventListener("ln-api-queue:request-enqueue",e._handlers.enqueue),e.dom.removeEventListener("ln-api-queue:ack",e._handlers.ack),e.dom.removeEventListener("ln-api-queue:nack",e._handlers.nack),e.dom.removeEventListener("ln-api-queue:request-remap",e._handlers.remap),e.dom.removeEventListener("ln-api-queue:resolve-create",e._handlers.resolveCreate),e.dom.removeEventListener("ln-api-queue:request-resume",e._handlers.resume),e.dom.removeEventListener("ln-api-queue:request-drain",e._handlers.drain),e.dom.removeEventListener("ln-api-queue:request-clear",e._handlers.clear),window.removeEventListener("online",e._onlineHandler),e._timers.forEach(n=>clearTimeout(n)),e._timers.clear(),y(e.dom,"ln-api-queue:destroyed",{scope:e.scope}),delete e.dom[t]};function d(e){const n=e[t];n&&n._drain()}j(r,t,i,"ln-api-queue",{extraAttributes:["data-ln-api-queue-online"],onAttributeChange:d})})()})();
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { registerComponent, dispatch } from '../../ln-core';
|
|
2
|
+
import { QueueStorage } from './queue-storage';
|
|
3
|
+
|
|
4
|
+
(function () {
|
|
5
|
+
const DOM_SELECTOR = 'data-ln-api-queue';
|
|
6
|
+
const DOM_ATTRIBUTE = 'lnApiQueue';
|
|
7
|
+
const BACKOFF_LADDER = [2000, 5000, 15000, 60000, 300000];
|
|
8
|
+
const MAX_ATTEMPTS = 8;
|
|
9
|
+
const LEASE_MS = 60000;
|
|
10
|
+
|
|
11
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
12
|
+
|
|
13
|
+
function _uuid() {
|
|
14
|
+
try { return crypto.randomUUID(); }
|
|
15
|
+
catch (_) {
|
|
16
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
17
|
+
const r = Math.random() * 16 | 0;
|
|
18
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const _storage = new QueueStorage({
|
|
24
|
+
indexedDB: window.indexedDB,
|
|
25
|
+
IDBKeyRange: window.IDBKeyRange,
|
|
26
|
+
uuid: _uuid
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function _component(dom) {
|
|
30
|
+
this.dom = dom;
|
|
31
|
+
dom[DOM_ATTRIBUTE] = this;
|
|
32
|
+
|
|
33
|
+
const coordinatorEl = dom.closest('[data-ln-data-coordinator]');
|
|
34
|
+
this.scope = dom.getAttribute(DOM_SELECTOR) || (coordinatorEl ? coordinatorEl.getAttribute('data-ln-data-coordinator') : null) || 'default';
|
|
35
|
+
|
|
36
|
+
this._paused = false;
|
|
37
|
+
this._timers = new Map();
|
|
38
|
+
this._workerId = _uuid();
|
|
39
|
+
this._drainPromise = null;
|
|
40
|
+
this._onlineHandler = () => this._drain();
|
|
41
|
+
|
|
42
|
+
this._bindEvents();
|
|
43
|
+
window.addEventListener('online', this._onlineHandler);
|
|
44
|
+
|
|
45
|
+
const self = this;
|
|
46
|
+
_storage.open()
|
|
47
|
+
.then(db => {
|
|
48
|
+
if (!db) {
|
|
49
|
+
console.warn('[ln-api-queue] IndexedDB not available — queue disabled');
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return _storage.getPaused(self.scope);
|
|
53
|
+
})
|
|
54
|
+
.then(paused => {
|
|
55
|
+
self._paused = !!paused;
|
|
56
|
+
if (self._paused) dispatch(self.dom, 'ln-api-queue:paused', { reason: 'auth', restored: true });
|
|
57
|
+
return self._emitPendingCount();
|
|
58
|
+
})
|
|
59
|
+
.then(() => self._drain())
|
|
60
|
+
.catch(error => {
|
|
61
|
+
console.error('[ln-api-queue] Initialization failed:', error);
|
|
62
|
+
dispatch(self.dom, 'ln-api-queue:error', { operation: 'initialize', error });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_component.prototype._isOnline = function () {
|
|
69
|
+
const attr = this.dom.getAttribute('data-ln-api-queue-online');
|
|
70
|
+
if (attr === 'true') return true;
|
|
71
|
+
if (attr === 'false') return false;
|
|
72
|
+
return navigator.onLine;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
_component.prototype._emitPendingCount = function () {
|
|
76
|
+
const self = this;
|
|
77
|
+
return _storage.allForScope(self.scope).then(entries => {
|
|
78
|
+
dispatch(self.dom, 'ln-api-queue:pending-count', { count: entries.length, scope: self.scope });
|
|
79
|
+
if (entries.length === 0) {
|
|
80
|
+
dispatch(self.dom, 'ln-api-queue:drained', { scope: self.scope });
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
_component.prototype._clearTimer = function (chainKey) {
|
|
87
|
+
const timer = this._timers.get(chainKey);
|
|
88
|
+
if (timer) {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
this._timers.delete(chainKey);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
_component.prototype._scheduleTimer = function (chainKey, delta) {
|
|
95
|
+
const delay = Math.max(0, delta);
|
|
96
|
+
const current = this._timers.get(chainKey);
|
|
97
|
+
if (current) clearTimeout(current);
|
|
98
|
+
|
|
99
|
+
const self = this;
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
self._timers.delete(chainKey);
|
|
102
|
+
self._drain();
|
|
103
|
+
}, delay);
|
|
104
|
+
this._timers.set(chainKey, timer);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
_component.prototype._drain = function () {
|
|
108
|
+
const self = this;
|
|
109
|
+
if (self._paused || !self._isOnline()) return Promise.resolve();
|
|
110
|
+
if (self._drainPromise) return self._drainPromise;
|
|
111
|
+
|
|
112
|
+
self._drainPromise = _storage.claimReady(self.scope, self._workerId, LEASE_MS)
|
|
113
|
+
.then(result => {
|
|
114
|
+
for (const wakeup of result.wakeups) {
|
|
115
|
+
self._scheduleTimer(wakeup.chainKey, wakeup.at - Date.now());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const entry of result.entries) {
|
|
119
|
+
self._clearTimer(entry.chainKey);
|
|
120
|
+
dispatch(self.dom, 'ln-api-queue:send', {
|
|
121
|
+
entryId: entry.entryId,
|
|
122
|
+
chainKey: entry.chainKey,
|
|
123
|
+
op: entry.op,
|
|
124
|
+
targetId: entry.targetId,
|
|
125
|
+
payload: entry.payload,
|
|
126
|
+
expectedVersion: entry.expectedVersion,
|
|
127
|
+
idempotencyKey: entry.entryId,
|
|
128
|
+
meta: entry.meta
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
.catch(error => {
|
|
133
|
+
console.error('[ln-api-queue] Drain failed:', error);
|
|
134
|
+
dispatch(self.dom, 'ln-api-queue:error', { operation: 'drain', error });
|
|
135
|
+
})
|
|
136
|
+
.finally(() => {
|
|
137
|
+
self._drainPromise = null;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return self._drainPromise;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
_component.prototype._onEnqueue = function (e) {
|
|
144
|
+
const self = this;
|
|
145
|
+
return _storage.enqueue(self.scope, e.detail || {})
|
|
146
|
+
.then(entry => {
|
|
147
|
+
if (!entry) return;
|
|
148
|
+
return self._emitPendingCount().then(entries => {
|
|
149
|
+
dispatch(self.dom, 'ln-api-queue:enqueued', {
|
|
150
|
+
entryId: entry.entryId,
|
|
151
|
+
chainKey: entry.chainKey,
|
|
152
|
+
count: entries.length
|
|
153
|
+
});
|
|
154
|
+
return self._drain();
|
|
155
|
+
});
|
|
156
|
+
})
|
|
157
|
+
.catch(error => {
|
|
158
|
+
dispatch(self.dom, 'ln-api-queue:error', { operation: 'enqueue', error });
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
_component.prototype._onAck = function (e) {
|
|
163
|
+
const self = this;
|
|
164
|
+
const detail = e.detail || {};
|
|
165
|
+
return _storage.ack(self.scope, detail.entryId)
|
|
166
|
+
.then(() => self._emitPendingCount())
|
|
167
|
+
.then(() => self._drain())
|
|
168
|
+
.catch(error => {
|
|
169
|
+
dispatch(self.dom, 'ln-api-queue:error', { operation: 'ack', entryId: detail.entryId, error });
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
_component.prototype._onNack = function (e) {
|
|
174
|
+
const self = this;
|
|
175
|
+
const detail = e.detail || {};
|
|
176
|
+
|
|
177
|
+
return _storage.nack(self.scope, detail.entryId, detail.reason, {
|
|
178
|
+
maxAttempts: MAX_ATTEMPTS,
|
|
179
|
+
backoff: BACKOFF_LADDER
|
|
180
|
+
}).then(result => {
|
|
181
|
+
if (!result) return;
|
|
182
|
+
|
|
183
|
+
if (result.status === 'failed') {
|
|
184
|
+
dispatch(self.dom, 'ln-api-queue:failed', {
|
|
185
|
+
entryId: result.entry.entryId,
|
|
186
|
+
chainKey: result.entry.chainKey,
|
|
187
|
+
attempts: result.entry.attempts
|
|
188
|
+
});
|
|
189
|
+
} else if (result.status === 'retry') {
|
|
190
|
+
self._scheduleTimer(result.entry.chainKey, result.delay);
|
|
191
|
+
} else if (result.status === 'auth') {
|
|
192
|
+
self._paused = true;
|
|
193
|
+
dispatch(self.dom, 'ln-api-queue:paused', { reason: 'auth' });
|
|
194
|
+
dispatch(self.dom, 'ln-api-queue:auth-required', {
|
|
195
|
+
entryId: result.entry.entryId,
|
|
196
|
+
chainKey: result.entry.chainKey
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return self._emitPendingCount().then(() => {
|
|
201
|
+
if (result.status === 'dropped') return self._drain();
|
|
202
|
+
});
|
|
203
|
+
}).catch(error => {
|
|
204
|
+
dispatch(self.dom, 'ln-api-queue:error', { operation: 'nack', entryId: detail.entryId, error });
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
_component.prototype._onRemap = function (e) {
|
|
209
|
+
const self = this;
|
|
210
|
+
const detail = e.detail || {};
|
|
211
|
+
return _storage.remap(self.scope, detail.oldKey, detail.newId)
|
|
212
|
+
.catch(error => {
|
|
213
|
+
dispatch(self.dom, 'ln-api-queue:error', { operation: 'remap', error });
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
_component.prototype._onResolveCreate = function (e) {
|
|
218
|
+
const self = this;
|
|
219
|
+
const detail = e.detail || {};
|
|
220
|
+
return _storage.resolveCreate(self.scope, detail.entryId, detail.oldKey, detail.newId)
|
|
221
|
+
.then(() => self._emitPendingCount())
|
|
222
|
+
.then(() => self._drain())
|
|
223
|
+
.catch(error => {
|
|
224
|
+
dispatch(self.dom, 'ln-api-queue:error', {
|
|
225
|
+
operation: 'resolve-create',
|
|
226
|
+
entryId: detail.entryId,
|
|
227
|
+
error
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
_component.prototype._onResume = function () {
|
|
233
|
+
const self = this;
|
|
234
|
+
return _storage.setPaused(self.scope, false).then(() => {
|
|
235
|
+
self._paused = false;
|
|
236
|
+
dispatch(self.dom, 'ln-api-queue:resumed', {});
|
|
237
|
+
return self._drain();
|
|
238
|
+
}).catch(error => {
|
|
239
|
+
dispatch(self.dom, 'ln-api-queue:error', { operation: 'resume', error });
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
_component.prototype._onDrain = function () {
|
|
244
|
+
const self = this;
|
|
245
|
+
return _storage.resetFailed(self.scope).then(() => {
|
|
246
|
+
const activeDrain = self._drainPromise;
|
|
247
|
+
return activeDrain ? activeDrain.then(() => self._drain()) : self._drain();
|
|
248
|
+
}).catch(error => {
|
|
249
|
+
dispatch(self.dom, 'ln-api-queue:error', { operation: 'manual-drain', error });
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
_component.prototype._onClear = function () {
|
|
254
|
+
const self = this;
|
|
255
|
+
self._timers.forEach(timer => clearTimeout(timer));
|
|
256
|
+
self._timers.clear();
|
|
257
|
+
return _storage.clear(self.scope).then(() => {
|
|
258
|
+
self._paused = false;
|
|
259
|
+
dispatch(self.dom, 'ln-api-queue:pending-count', { count: 0, scope: self.scope });
|
|
260
|
+
dispatch(self.dom, 'ln-api-queue:drained', { scope: self.scope });
|
|
261
|
+
}).catch(error => {
|
|
262
|
+
dispatch(self.dom, 'ln-api-queue:error', { operation: 'clear', error });
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
_component.prototype._bindEvents = function () {
|
|
267
|
+
const self = this;
|
|
268
|
+
self._handlers = {
|
|
269
|
+
enqueue: e => self._onEnqueue(e),
|
|
270
|
+
ack: e => self._onAck(e),
|
|
271
|
+
nack: e => self._onNack(e),
|
|
272
|
+
remap: e => self._onRemap(e),
|
|
273
|
+
resolveCreate: e => self._onResolveCreate(e),
|
|
274
|
+
resume: () => self._onResume(),
|
|
275
|
+
drain: () => self._onDrain(),
|
|
276
|
+
clear: () => self._onClear()
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
self.dom.addEventListener('ln-api-queue:request-enqueue', self._handlers.enqueue);
|
|
280
|
+
self.dom.addEventListener('ln-api-queue:ack', self._handlers.ack);
|
|
281
|
+
self.dom.addEventListener('ln-api-queue:nack', self._handlers.nack);
|
|
282
|
+
self.dom.addEventListener('ln-api-queue:request-remap', self._handlers.remap);
|
|
283
|
+
self.dom.addEventListener('ln-api-queue:resolve-create', self._handlers.resolveCreate);
|
|
284
|
+
self.dom.addEventListener('ln-api-queue:request-resume', self._handlers.resume);
|
|
285
|
+
self.dom.addEventListener('ln-api-queue:request-drain', self._handlers.drain);
|
|
286
|
+
self.dom.addEventListener('ln-api-queue:request-clear', self._handlers.clear);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
_component.prototype.destroy = function () {
|
|
290
|
+
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
291
|
+
|
|
292
|
+
const self = this;
|
|
293
|
+
self.dom.removeEventListener('ln-api-queue:request-enqueue', self._handlers.enqueue);
|
|
294
|
+
self.dom.removeEventListener('ln-api-queue:ack', self._handlers.ack);
|
|
295
|
+
self.dom.removeEventListener('ln-api-queue:nack', self._handlers.nack);
|
|
296
|
+
self.dom.removeEventListener('ln-api-queue:request-remap', self._handlers.remap);
|
|
297
|
+
self.dom.removeEventListener('ln-api-queue:resolve-create', self._handlers.resolveCreate);
|
|
298
|
+
self.dom.removeEventListener('ln-api-queue:request-resume', self._handlers.resume);
|
|
299
|
+
self.dom.removeEventListener('ln-api-queue:request-drain', self._handlers.drain);
|
|
300
|
+
self.dom.removeEventListener('ln-api-queue:request-clear', self._handlers.clear);
|
|
301
|
+
window.removeEventListener('online', self._onlineHandler);
|
|
302
|
+
|
|
303
|
+
self._timers.forEach(timer => clearTimeout(timer));
|
|
304
|
+
self._timers.clear();
|
|
305
|
+
|
|
306
|
+
dispatch(self.dom, 'ln-api-queue:destroyed', { scope: self.scope });
|
|
307
|
+
delete self.dom[DOM_ATTRIBUTE];
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function _syncAttr(el) {
|
|
311
|
+
const instance = el[DOM_ATTRIBUTE];
|
|
312
|
+
if (!instance) return;
|
|
313
|
+
instance._drain();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-api-queue', {
|
|
317
|
+
extraAttributes: ['data-ln-api-queue-online'],
|
|
318
|
+
onAttributeChange: _syncAttr
|
|
319
|
+
});
|
|
320
|
+
})();
|