@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,46 @@
|
|
|
1
|
+
export class MutationReceipts {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._pending = new Map();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
wait(requestId) {
|
|
7
|
+
if (!requestId) return Promise.reject(new Error('Mutation requestId is required'));
|
|
8
|
+
if (this._pending.has(requestId)) {
|
|
9
|
+
return Promise.reject(new Error(`Duplicate mutation requestId: ${requestId}`));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
this._pending.set(requestId, { resolve, reject });
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
resolve(detail) {
|
|
18
|
+
return this._settle(detail, false);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
reject(detail) {
|
|
22
|
+
return this._settle(detail, true);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
close(error) {
|
|
26
|
+
const reason = error || new Error('Mutation receipt registry closed');
|
|
27
|
+
for (const receipt of this._pending.values()) receipt.reject(reason);
|
|
28
|
+
this._pending.clear();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_settle(detail, failed) {
|
|
32
|
+
const requestId = detail && detail.requestId;
|
|
33
|
+
if (!requestId) return false;
|
|
34
|
+
|
|
35
|
+
const receipt = this._pending.get(requestId);
|
|
36
|
+
if (!receipt) return false;
|
|
37
|
+
|
|
38
|
+
this._pending.delete(requestId);
|
|
39
|
+
if (failed) {
|
|
40
|
+
receipt.reject(detail.error || new Error('Store mutation failed'));
|
|
41
|
+
} else {
|
|
42
|
+
receipt.resolve(detail);
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,94 +1,241 @@
|
|
|
1
|
-
# ln-data-store
|
|
2
|
-
|
|
3
|
-
A zero-dependency, local-first **Database Cache Store** backed by standard browser `IndexedDB`. It acts as a pure client-side database cache, maintaining records locally, executing high-performance querying in-memory, and
|
|
4
|
-
|
|
5
|
-
It possesses no visual interface and is **completely blind to the network** (no fetch, status codes, paths, or
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 📦 Declarative Setup in HTML
|
|
10
|
-
|
|
11
|
-
```html
|
|
12
|
-
<div data-ln-data-store="documents"
|
|
13
|
-
data-ln-data-store-stale="300"
|
|
14
|
-
data-ln-data-store-indexes="status,department,updated_at"
|
|
15
|
-
data-ln-data-store-search-fields="title,owner">
|
|
16
|
-
</div>
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
store.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
1
|
+
# ln-data-store
|
|
2
|
+
|
|
3
|
+
A zero-dependency, local-first **Database Cache Store** backed by standard browser `IndexedDB`. It acts as a pure client-side database cache, maintaining records locally, executing high-performance querying in-memory, and applying optimistic mutations directly — a record is a record, with no pending state, snapshots, or rollback machinery.
|
|
4
|
+
|
|
5
|
+
It possesses no visual interface and is **completely blind to the network** (no fetch, status codes, paths, or URLs). Instead, it communicates strictly via custom DOM events, allowing the parent **Data Coordinator** to orchestrate syncs and mutations.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📦 Declarative Setup in HTML
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<div data-ln-data-store="documents"
|
|
13
|
+
data-ln-data-store-stale="300"
|
|
14
|
+
data-ln-data-store-indexes="status,department,updated_at"
|
|
15
|
+
data-ln-data-store-search-fields="title,owner">
|
|
16
|
+
</div>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🌐 Declarative Attributes
|
|
22
|
+
|
|
23
|
+
| Attribute | Description | Default |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `data-ln-data-store="name"` | Declares the store name. | *Required* |
|
|
26
|
+
| `data-ln-data-store-stale="N"` | Seconds before cache is considered stale. Set to `never` or `-1` to disable staleness. | `300` |
|
|
27
|
+
| `data-ln-data-store-indexes="…"` | Comma-separated list of IndexedDB index fields. | `""` |
|
|
28
|
+
| `data-ln-data-store-search-fields="…"` | Comma-separated list of text-search fields for local query search matching. | `""` |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 🗄️ Dynamic IndexedDB Schema Management
|
|
33
|
+
|
|
34
|
+
`ln-data-store` registers and operates on a single IndexedDB database named `ln_app_cache`.
|
|
35
|
+
|
|
36
|
+
- **Dynamic Schema Discovery:** Upon initialization, the component scans the document for all `[data-ln-data-store]` elements. It collects their store names and index definitions.
|
|
37
|
+
- **Auto Upgrade:** If new stores or indexes are declared in the HTML markup, the component closes the active connection, increments the database version, and dynamically creates the missing stores and indexes in `onupgradeneeded`.
|
|
38
|
+
- **Memory Fallback:** If the browser does not support IndexedDB, or if database open/upgrade operations fail/are blocked, the component transparently falls back to an in-memory data store.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 🔒 At-Rest AES-GCM Encryption
|
|
43
|
+
|
|
44
|
+
The store supports local record encryption to secure cached data in IndexedDB:
|
|
45
|
+
|
|
46
|
+
- **Activation:** Set a storage key by calling `window.lnCore.setStorageKey(key)` or `window.lnDataStore.setStorageKey(key)`. The key is derived using a SHA-256 hash of the input string.
|
|
47
|
+
- **Indexable Payload:** Only the data properties of the record are encrypted using `AES-GCM` (using helper methods from `ln-core`). The record's primary identifier `id` is preserved in plain text to allow IndexedDB index lookups and queries.
|
|
48
|
+
- **Automatic Decryption:** All queries and retrievals automatically decrypt records if a storage key is active.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 🛠️ JS API (Access via `el.lnDataStore`)
|
|
53
|
+
|
|
54
|
+
Access the database layer directly via the `lnDataStore` property on the store container element:
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
const storeEl = document.querySelector('[data-ln-data-store="documents"]');
|
|
58
|
+
const store = storeEl.lnDataStore;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 1. In-Memory Query Engine
|
|
62
|
+
|
|
63
|
+
Queries execute against in-memory copies of the IndexedDB records for near-zero latency:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
const { data, total, filtered } = await store.getAll({
|
|
67
|
+
sort: { field: 'updated_at', direction: 'desc' },
|
|
68
|
+
filters: { status: ['Approved', 'Pending'] },
|
|
69
|
+
search: 'ISO 27001',
|
|
70
|
+
offset: 0,
|
|
71
|
+
limit: 50
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Natural Sorting:** Sorting uses `Intl.Collator` configured with `{ numeric: true, sensitivity: 'base' }` for natural alphabetical/numeric ordering. `null` and `undefined` values are correctly sorted to the end of the collection (or the beginning if sorting descending).
|
|
76
|
+
- **Filtering:** Filters perform exact string matches against arrays of values.
|
|
77
|
+
- **Text Search:** Compares the lowercase query string against the lowercase values of fields declared under `data-ln-data-store-search-fields`.
|
|
78
|
+
|
|
79
|
+
### 2. Presenters / Decorators
|
|
80
|
+
|
|
81
|
+
Configure computed virtual fields that are dynamically appended to records when queried:
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
store.setPresenters({
|
|
85
|
+
computed: {
|
|
86
|
+
size_display: record => (record.file_size / 1024).toFixed(1) + ' MB',
|
|
87
|
+
full_name: record => `${record.first_name} ${record.last_name}`
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Public Methods
|
|
93
|
+
|
|
94
|
+
| Method | Return Type | Description |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `getAll(options)` | `Promise<Object>` | Retrieves records. Options support `sort` (`{ field, direction }`), `filters` (`{ field: Array }`), `search` (`String`), `offset`, `limit`. Returns `{ data, total, filtered }`. |
|
|
97
|
+
| `getById(id)` | `Promise<Object\|null>` | Returns a single record (decorated with presenters) or `null`. |
|
|
98
|
+
| `count(filters)` | `Promise<Number>` | Returns the total count of records. If filters are provided, returns the filtered count. |
|
|
99
|
+
| `aggregate(field, fn)` | `Promise<Number>` | Performs aggregation. `fn` must be `'count'`, `'sum'`, or `'avg'`. |
|
|
100
|
+
| `setPresenters(presenters)` | `void` | Registers presenters (decorators) for computed fields. |
|
|
101
|
+
| `applySync(upserted, deleted, syncedAt)` | `Promise<void>` | Feeds synchronization delta updates into the cache. |
|
|
102
|
+
| `forceSync()` | `void` | Dispatches `ln-data-store:request-remote-sync` with the current last sync timestamp. |
|
|
103
|
+
| `fullReload()` | `Promise<void>` | Clears the IndexedDB store, resets sync metadata, and triggers a sync. |
|
|
104
|
+
| `destroy()` | `void` | Cleans up the instance, removes event listeners, and deletes the DOM reference. |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## ⚡ DOM Events
|
|
109
|
+
|
|
110
|
+
### Commands (Listened to by the Store)
|
|
111
|
+
|
|
112
|
+
All mutations must be routed via DOM events. **Never invoke write methods directly.** The caller is responsible for supplying a `tempId` (e.g. `'_temp_' + crypto.randomUUID()`) when creating records:
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
// Create record optimistically
|
|
116
|
+
store.dispatchEvent(new CustomEvent('ln-data-store:request-create', {
|
|
117
|
+
detail: {
|
|
118
|
+
tempId: '_temp_abc123',
|
|
119
|
+
data: { title: 'New Document', status: 'Draft' }
|
|
120
|
+
}
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
// Update record optimistically
|
|
124
|
+
// Note: If data.id is different from the target id, the store performs an atomic id-swap (rekey)
|
|
125
|
+
store.dispatchEvent(new CustomEvent('ln-data-store:request-update', {
|
|
126
|
+
detail: {
|
|
127
|
+
id: '_temp_abc123',
|
|
128
|
+
data: { id: 42, title: 'Server Confirmed Document' }
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
// Delete record optimistically
|
|
133
|
+
store.dispatchEvent(new CustomEvent('ln-data-store:request-delete', {
|
|
134
|
+
detail: { id: 42 }
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// Bulk delete records optimistically
|
|
138
|
+
store.dispatchEvent(new CustomEvent('ln-data-store:request-bulk-delete', {
|
|
139
|
+
detail: { ids: [42, 43, 44] }
|
|
140
|
+
}));
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Every mutation command may include an opaque `requestId`. The store serializes
|
|
144
|
+
the write, persists record-count metadata, and then echoes that id on exactly
|
|
145
|
+
one terminal notification: `created` / `updated` / `deleted`, or
|
|
146
|
+
`mutation-error`. The protocol has no wall-clock timeout; lifecycle owners must
|
|
147
|
+
cancel their own pending receipts when they are destroyed.
|
|
148
|
+
|
|
149
|
+
An empty bulk-delete is an explicit no-op: it returns a resolved mutation,
|
|
150
|
+
emits `deleted {ids: []}`, and does not rewrite record-count metadata because
|
|
151
|
+
no local state changed.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### Notifications (Emitted by the Store)
|
|
156
|
+
|
|
157
|
+
These events bubble up and can be listened to by coordinators or rendering views (`ln-table` / `ln-list`):
|
|
158
|
+
|
|
159
|
+
| Event | `e.detail` payload | Description |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `ln-data-store:initialized` | `{ store, hasCache, lastSyncedAt, count }` | Emitted once after IndexedDB connection opens. The instance's `ready` promise resolves after this state is committed. |
|
|
162
|
+
| `ln-data-store:initialization-error` | `{ store, error }` | IndexedDB could not be initialized. `ready` still resolves after `initializationError` is set, allowing a coordinator to route reads to its connector. |
|
|
163
|
+
| `ln-data-store:ready` | `{ store, count, source }` | Emitted when data is ready. `source` is `'cache'` (init) or `'server'` (first sync). |
|
|
164
|
+
| `ln-data-store:loaded` | `{ store, count }` | Emitted on initial load sync completion (first sync). |
|
|
165
|
+
| `ln-data-store:created` | `{ store, record, tempId, requestId? }` | Emitted after optimistic creation. |
|
|
166
|
+
| `ln-data-store:updated` | `{ store, record, previous, requestId? }` | Emitted after optimistic update or id-swap rekey. |
|
|
167
|
+
| `ln-data-store:deleted` | `{ store, id \| ids, requestId? }` | Emitted after optimistic delete or bulk delete. |
|
|
168
|
+
| `ln-data-store:synced` | `{ store, added, deleted, changed }` | Emitted after subsequent delta sync merges. |
|
|
169
|
+
| `ln-data-store:sync-error` | `{ store, error, status }` | A connector sync failed; `isSyncing` has been cleared so online/visibility retry can proceed. |
|
|
170
|
+
| `ln-data-store:destroyed` | `{ store }` | Emitted when the store instance is destroyed. |
|
|
171
|
+
| `ln-data-store:mutation-error` | `{ store, action, requestId, error }` | A serialized local mutation failed. `requestId` lets a coordinator correlate reconciliation failures. |
|
|
172
|
+
|
|
173
|
+
### Global System Events
|
|
174
|
+
|
|
175
|
+
| Event | Target | `e.detail` payload | Description |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `ln-data-store:quota-exceeded` | `document` | `{ error }` | Emitted globally on `document` if database storage exceeds browser quotas. |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 🌐 Global API (On `window.lnDataStore`)
|
|
182
|
+
|
|
183
|
+
| Static Method | Description |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `window.lnDataStore.clearAll()` | Clears all records and metadata from all stores in the IndexedDB database. |
|
|
186
|
+
| `window.lnDataStore.setStorageKey(key)` | Sets the global cryptographic key for record encryption. |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 🔧 Internals
|
|
191
|
+
|
|
192
|
+
Source: `js/ln-data-store/ln-data-store.js`.
|
|
193
|
+
|
|
194
|
+
### Shared IndexedDB connection
|
|
195
|
+
|
|
196
|
+
The page holds one `IDBDatabase` connection (`ln_app_cache`) shared by every `[data-ln-data-store]` instance; each store scopes its own transactions by name via a shared `_getDb()`. A module-level `_stores` registry (`{name: instance}`) exists purely for lookup — it does not drive any cross-store sync.
|
|
197
|
+
|
|
198
|
+
### No self-sync
|
|
199
|
+
|
|
200
|
+
`_initStore` only emits `ln-data-store:initialized` — in every branch (cache present, empty, schema-mismatch-after-clear) — it never triggers a remote sync itself. Deciding WHEN to sync (on init, on `online`, on `visibilitychange`) is entirely `ln-data-coordinator`'s job; the store keeps no visibility/online listeners of its own. `_triggerRemoteSync` (which dispatches `ln-data-store:request-remote-sync`) is reachable only through the two public commands `forceSync()` / `fullReload()` — always consumer-initiated, never automatic.
|
|
201
|
+
|
|
202
|
+
### Instance state
|
|
203
|
+
|
|
204
|
+
`storeEl.lnDataStore` carries `ready` (initialization promise),
|
|
205
|
+
`isInitialized` (metadata inspection completed), `initializationError` (the
|
|
206
|
+
IndexedDB failure, or `null`), `hasCache` (a local snapshot
|
|
207
|
+
exists, including an intentionally empty snapshot), `isLoaded` (the snapshot
|
|
208
|
+
is queryable), `isSyncing` (a remote-sync request is outstanding — set when
|
|
209
|
+
the store emits `ln-data-store:request-remote-sync`, cleared on `applySync`),
|
|
210
|
+
`lastSyncedAt` (the last sync watermark, passed to the coordinator as the
|
|
211
|
+
`since` field of that request event — the store itself never fetches), and
|
|
212
|
+
`totalCount`.
|
|
213
|
+
|
|
214
|
+
Local mutations are serialized per store instance. After the record write,
|
|
215
|
+
the store recounts and persists `_meta` before emitting the post-event. The
|
|
216
|
+
mutation chain waits for `ready`; an unavailable store emits `mutation-error`
|
|
217
|
+
instead of reporting a no-op write as successful.
|
|
218
|
+
Reconciliation commands may include a `requestId`; the same id is returned on
|
|
219
|
+
`created`/`updated`/`deleted` or `mutation-error`.
|
|
220
|
+
|
|
221
|
+
`hasCache` is deliberately not derived from `record_count`. A synchronized or
|
|
222
|
+
optimistically-mutated snapshot with zero rows is still authoritative local
|
|
223
|
+
state; treating it as "no cache" would allow a remote query to resurrect rows
|
|
224
|
+
that were just deleted. Only initialization without metadata, schema reset,
|
|
225
|
+
`clearAll`, and `fullReload` transition the instance back to no-snapshot state.
|
|
226
|
+
|
|
227
|
+
### Encryption pipeline
|
|
228
|
+
|
|
229
|
+
When a storage key is active: `_putRecord`/`_putBulk` keep `id` in plaintext (IndexedDB keys/indexes must stay queryable) and encrypt the rest of the payload with a fresh random 12-byte IV — `{ id, encrypted: true, iv, data }`. An unsynced record is identified solely by its `_temp_`-prefixed `id` (itself plaintext) — there is no separate pending/sync-state marker field. `_getAllRecords`/`_getRecord` decrypt in memory before a record reaches the in-memory sort/filter/search engine, so query behavior is identical whether encryption is active or not.
|
|
230
|
+
|
|
231
|
+
### DOM mutations
|
|
232
|
+
|
|
233
|
+
The store renders nothing. Its only DOM-side effects: attaches `request-*` listeners on the store element, writes `dom.lnDataStore = instance`, and dispatches `ln-data-store:*` events on the store element (`quota-exceeded` dispatches on `document` instead).
|
|
234
|
+
|
|
235
|
+
### Mutation error reconciliation
|
|
236
|
+
|
|
237
|
+
`ln-data-store` has no concept of "confirm" or "revert" — a `request-update`/`request-delete` is applied unconditionally to the local cache. Reconciling server-side mutation errors (retry, drop, conflict-server-wins, auth-pause) is `ln-data-coordinator`'s job — see [`js/ln-data-coordinator/README.md`](../ln-data-coordinator/README.md) §Error reconciliation policy.
|
|
238
|
+
|
|
239
|
+
### At-rest encryption reference
|
|
240
|
+
|
|
241
|
+
For encryption threat model and operational guidance, see [Security Architecture & Best Practices](../../docs/architecture/security.md).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function l(s,a,d){s.dispatchEvent(new CustomEvent(a,{bubbles:!0,detail:d||{}}))}function H(s,a){if(!document.body){document.addEventListener("DOMContentLoaded",function(){H(s,a)}),console.warn("["+a+'] Script loaded before <body> — add "defer" to your <script> tag');return}s()}function U(s,a,d,h){if(s.nodeType!==1)return;const _=a.indexOf("[")!==-1||a.indexOf(".")!==-1||a.indexOf("#")!==-1?a:"["+a+"]",m=Array.from(s.querySelectorAll(_));s.matches&&s.matches(_)&&m.push(s);for(const u of m)u[d]||(u[d]=new h(u))}function ee(s,a,d,h,y={}){const _=y.extraAttributes||[],m=y.onAttributeChange||null,u=y.onInit||null;function g(j){const D=j||document.body;U(D,s,a,d),u&&u(D)}return H(function(){const j=new MutationObserver(function(B){for(let C=0;C<B.length;C++){const p=B[C];if(p.type==="childList"){for(let w=0;w<p.addedNodes.length;w++){const S=p.addedNodes[w];S.nodeType===1&&(U(S,s,a,d),u&&u(S))}for(let w=0;w<p.removedNodes.length;w++){const S=p.removedNodes[w];if(S.nodeType===1){const v=s.indexOf("[")!==-1||s.indexOf(".")!==-1||s.indexOf("#")!==-1?s:"["+s+"]",R=Array.from(S.querySelectorAll(v));S.matches&&S.matches(v)&&R.push(S);for(let M=0;M<R.length;M++){const x=R[M];if(!document.contains(x)){const q=x[a];q&&typeof q.destroy=="function"&&q.destroy()}}}}}else p.type==="attributes"&&(m&&p.target[a]?m(p.target,p.attributeName):(U(p.target,s,a,d),u&&u(p.target)))}});let D=[];if(s.indexOf("[")!==-1){const B=/\[([\w-]+)/g;let C;for(;(C=B.exec(s))!==null;)D.push(C[1])}else D.push(s);j.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:D.concat(_)})},h),window[a]=g,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){g(document.body)}):g(document.body),g}const V={};function te(s,a){V[s]=a}function ne(s){return V[s]||{ingress:a=>a,egress:a=>a}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=te,window.lnCore.getDataMapper=ne);let k=null;async function G(s){if(!s){k=null;return}try{const a=new TextEncoder,d=await crypto.subtle.digest("SHA-256",a.encode(s));k=await crypto.subtle.importKey("raw",d,{name:"AES-GCM"},!1,["encrypt","decrypt"])}catch(a){console.error("[ln-core/crypto] Key derivation failed:",a),k=null}}function T(){return k}async function re(s,a=k){const d=a||k;if(!d||s===void 0||s===null)return s;try{const h=new TextEncoder,y=crypto.getRandomValues(new Uint8Array(12)),_=typeof s=="string"?s:JSON.stringify(s),m=await crypto.subtle.encrypt({name:"AES-GCM",iv:y},d,h.encode(_)),u=btoa(String.fromCharCode(...y)),g=btoa(String.fromCharCode(...new Uint8Array(m)));return{encrypted:!0,iv:u,data:g}}catch(h){return console.error("[ln-core/crypto] Encryption failed:",h),s}}async function oe(s,a=k){const d=a||k;if(!s||!s.encrypted||!d)return s;try{const h=new TextDecoder,y=Uint8Array.from(atob(s.iv),g=>g.charCodeAt(0)),_=Uint8Array.from(atob(s.data),g=>g.charCodeAt(0)),m=await crypto.subtle.decrypt({name:"AES-GCM",iv:y},d,_),u=h.decode(m);try{return JSON.parse(u)}catch{return u}}catch(h){return console.error("[ln-core/crypto] Decryption failed. Key may be incorrect:",h),{...s,decryptionError:!0}}}(function(){const s="data-ln-data-store",a="lnDataStore";if(window[a]!==void 0)return;const d="ln_app_cache",h="_meta",y="1.0";let _=null,m=null;const u={};function g(){try{return crypto.randomUUID()}catch{return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,t=>{const r=Math.random()*16|0;return(t==="x"?r:r&3|8).toString(16)})}}function j(e){e&&e.name==="QuotaExceededError"&&l(document,"ln-store:quota-exceeded",{error:e})}function D(){const e={};for(const t of document.querySelectorAll(`[${s}]`)){const r=t.getAttribute(s);if(r){const n=t.getAttribute("data-ln-data-store-indexes")||t.getAttribute("data-ln-store-indexes")||"";e[r]={indexes:n.split(",").map(o=>o.trim()).filter(Boolean)}}}return e}function B(){return m||(m=new Promise(e=>{if(typeof indexedDB>"u")return console.warn("[ln-data-store] IndexedDB not available — falling back to in-memory store"),e(null);const t=D(),r=Object.keys(t),n=indexedDB.open(d);n.onerror=()=>{console.warn("[ln-data-store] IndexedDB open failed — falling back to in-memory store"),e(null)},n.onsuccess=o=>{const i=o.target.result,c=Array.from(i.objectStoreNames);if(!(!c.includes(h)||r.some(O=>!c.includes(O))))return C(i),_=i,e(i);const A=i.version;i.close();const P=indexedDB.open(d,A+1);P.onblocked=()=>{console.warn("[ln-data-store] Database upgrade blocked — waiting for other tabs to close connection")},P.onerror=()=>{console.warn("[ln-data-store] Database upgrade failed"),e(null)},P.onupgradeneeded=O=>{const L=O.target.result;L.objectStoreNames.contains(h)||L.createObjectStore(h,{keyPath:"key"});for(const $ of r)if(!L.objectStoreNames.contains($)){const _e=L.createObjectStore($,{keyPath:"id"});for(const I of t[$].indexes)_e.createIndex(I,I,{unique:!1})}},P.onsuccess=O=>{const L=O.target.result;C(L),_=L,e(L)}}}),m)}function C(e){e.onversionchange=()=>{e.close(),_=null,m=null}}function p(){return _?Promise.resolve(_):(m=null,B())}async function w(e){if(!T()||!e)return e;const t={...e},r=t.id,n=t._pending,o=await re(t);return!o||!o.encrypted?e:{id:r,_pending:n,encrypted:!0,iv:o.iv,data:o.data}}async function S(e){return!e||!e.encrypted||!T()?e:oe(e)}const E=(e,t)=>p().then(r=>r?r.transaction(e,t).objectStore(e):null);function v(e){return new Promise((t,r)=>{e.onsuccess=()=>t(e.result),e.onerror=()=>{j(e.error),r(e.error)}})}const R=e=>E(e,"readonly").then(t=>t?v(t.getAll()):[]).then(t=>T()?Promise.all(t.map(r=>S(r))):t),M=(e,t)=>E(e,"readonly").then(r=>r?v(r.get(t)):null).then(r=>r?S(r):null),x=(e,t)=>(T()?w(t):Promise.resolve(t)).then(n=>E(e,"readwrite").then(o=>o?v(o.put(n)):null)),q=(e,t)=>E(e,"readwrite").then(r=>r?v(r.delete(t)):null),J=e=>E(e,"readwrite").then(t=>t?v(t.clear()):null),Q=e=>E(e,"readonly").then(t=>t?v(t.count()):0),se=e=>E(h,"readonly").then(t=>t?v(t.get(e)):null),z=(e,t)=>E(h,"readwrite").then(r=>{if(r)return t.key=e,v(r.put(t))});function b(e){this.dom=e,this._name=e.getAttribute(s);const t=e.getAttribute("data-ln-data-store-stale")||e.getAttribute("data-ln-store-stale"),r=parseInt(t,10);this._staleThreshold=t==="never"||t==="-1"?-1:isNaN(r)?300:r;const n=e.getAttribute("data-ln-data-store-search-fields")||e.getAttribute("data-ln-store-search-fields")||"";return this._searchFields=n.split(",").map(o=>o.trim()).filter(Boolean),this._handlers=null,this._pendingSnapshots={},this.isLoaded=!1,this.isSyncing=!1,this.lastSyncedAt=null,this.totalCount=0,this.presenters=null,u[this._name]=this,ie(this),de(this),this}function ie(e){e._handlers={create:t=>ae(e,t.detail),update:t=>ce(e,t.detail),delete:t=>le(e,t.detail),"bulk-delete":t=>ue(e,t.detail)};for(const[t,r]of Object.entries(e._handlers))e.dom.addEventListener(`ln-store:request-${t}`,r)}function ae(e,{data:t={}}={}){const r=`_temp_${g()}`,n={...t,id:r,_pending:!0};x(e._name,n).then(()=>{e.totalCount++,l(e.dom,"ln-store:created",{store:e._name,record:n,tempId:r}),l(e.dom,"ln-store:request-remote-create",{tempId:r,data:t})})}function ce(e,{id:t,data:r={},expected_version:n}={}){M(e._name,t).then(o=>{if(!o)throw new Error(`Record not found: ${t}`);e._pendingSnapshots[t]={...o};const i={...o,...r,_pending:!0};return x(e._name,i).then(()=>{l(e.dom,"ln-store:updated",{store:e._name,record:i,previous:e._pendingSnapshots[t]}),l(e.dom,"ln-store:request-remote-update",{id:t,data:r,expected_version:n})})}).catch(o=>console.error("[ln-data-store] Optimistic update failed:",o))}function le(e,{id:t}={}){M(e._name,t).then(r=>{if(r)return e._pendingSnapshots[t]={...r},q(e._name,t).then(()=>{e.totalCount--,l(e.dom,"ln-store:deleted",{store:e._name,id:t}),l(e.dom,"ln-store:request-remote-delete",{id:t})})}).catch(r=>console.error("[ln-data-store] Optimistic delete failed:",r))}function ue(e,{ids:t=[]}={}){t.length&&Promise.all(t.map(r=>M(e._name,r))).then(r=>{const n=r.filter(Boolean),o=n.map(i=>i.id);return e._pendingSnapshots[o.join(",")]=n,Y(e._name,o).then(()=>{e.totalCount-=o.length,l(e.dom,"ln-store:deleted",{store:e._name,ids:o}),l(e.dom,"ln-store:request-remote-bulk-delete",{ids:o})})}).catch(r=>console.error("[ln-data-store] Optimistic bulk delete failed:",r))}function de(e){B().then(()=>se(e._name)).then(t=>{t&&t.schema_version===y?(e.lastSyncedAt=t.last_synced_at||null,e.totalCount=t.record_count||0,e.totalCount>0?(e.isLoaded=!0,l(e.dom,"ln-store:ready",{store:e._name,count:e.totalCount,source:"cache"}),W(e)&&N(e)):N(e)):t&&t.schema_version!==y?J(e._name).then(()=>z(e._name,{schema_version:y,last_synced_at:null,record_count:0})).then(()=>N(e)):N(e)})}function W(e){return e._staleThreshold===-1?!1:e.lastSyncedAt?Math.floor(Date.now()/1e3)-e.lastSyncedAt>e._staleThreshold:!0}function N(e){e.isSyncing=!0,l(e.dom,"ln-store:request-remote-sync",{since:e.lastSyncedAt})}function X(e,t){return p().then(r=>r?(T()?Promise.all(t.map(o=>w(o))):Promise.resolve(t)).then(o=>new Promise((i,c)=>{const f=r.transaction(e,"readwrite"),A=f.objectStore(e);o.forEach(P=>A.put(P)),f.oncomplete=()=>i(),f.onerror=()=>{j(f.error),c(f.error)}})):void 0)}function Y(e,t){return p().then(r=>{if(r)return new Promise((n,o)=>{const i=r.transaction(e,"readwrite"),c=i.objectStore(e);t.forEach(f=>c.delete(f)),i.oncomplete=()=>n(),i.onerror=()=>o(i.error)})})}let K=()=>{document.visibilityState==="visible"&&Object.values(u).forEach(e=>{e.isLoaded&&!e.isSyncing&&W(e)&&N(e)})};document.addEventListener("visibilitychange",K);const he=new Intl.Collator(void 0,{numeric:!0,sensitivity:"base"});function fe(e,t){if(!t||!t.field)return e;const{field:r,direction:n}=t,o=n==="desc";return[...e].sort((i,c)=>{const f=i[r],A=c[r];if(f==null&&A==null)return 0;if(f==null)return o?1:-1;if(A==null)return o?-1:1;const P=typeof f=="string"&&typeof A=="string"?he.compare(f,A):f<A?-1:f>A?1:0;return o?-P:P})}function Z(e,t){if(!t)return e;const r=Object.keys(t).filter(n=>Array.isArray(t[n])&&t[n].length>0);return r.length?e.filter(n=>r.every(o=>t[o].map(String).includes(String(n[o])))):e}function me(e,t,r){if(!t||!r||!r.length)return e;const n=t.toLowerCase();return e.filter(o=>r.some(i=>{const c=o[i];return c!=null&&String(c).toLowerCase().includes(n)}))}function pe(e,t,r){if(!e.length)return 0;if(r==="count")return e.length;const n=e.map(i=>parseFloat(i[t])).filter(i=>!isNaN(i)),o=n.reduce((i,c)=>i+c,0);return r==="sum"?o:r==="avg"&&n.length?o/n.length:0}function F(e,t){if(!e.presenters||!e.presenters.computed)return t;const r=e.presenters.computed;return t.map(n=>{const o={...n};for(const[i,c]of Object.entries(r))try{o[i]=c(n)}catch(f){console.error(`[ln-data-store] Decorator computed field failed for ${i}`,f)}return o})}b.prototype.getAll=function(e={}){const t=this;return R(t._name).then(r=>{const n=r.length;e.filters&&(r=Z(r,e.filters)),e.search&&(r=me(r,e.search,t._searchFields));const o=r.length;if(e.sort&&(r=fe(r,e.sort)),e.offset||e.limit){const i=e.offset||0,c=e.limit||r.length;r=r.slice(i,i+c)}return{data:F(t,r),total:n,filtered:o}})},b.prototype.getById=function(e){return M(this._name,e).then(t=>t?F(this,[t])[0]:null)},b.prototype.count=function(e){return e?R(this._name).then(t=>Z(t,e).length):Q(this._name)},b.prototype.aggregate=function(e,t){return R(this._name).then(r=>pe(r,e,t))},b.prototype.setPresenters=function(e){this.presenters=e},b.prototype.applySync=function(e,t,r){const n=this,o=e.length>0||t.length>0;let i=Promise.resolve();return e.length>0&&(i=i.then(()=>X(n._name,e))),t.length>0&&(i=i.then(()=>Y(n._name,t))),i.then(()=>Q(n._name)).then(c=>(n.totalCount=c,z(n._name,{schema_version:y,last_synced_at:r,record_count:c}))).then(()=>{const c=!n.isLoaded;n.isLoaded=!0,n.isSyncing=!1,n.lastSyncedAt=r,c?(l(n.dom,"ln-store:loaded",{store:n._name,count:n.totalCount}),l(n.dom,"ln-store:ready",{store:n._name,count:n.totalCount,source:"server"})):l(n.dom,"ln-store:synced",{store:n._name,added:e.length,deleted:t.length,changed:o})}).catch(c=>{n.isSyncing=!1,console.error("[ln-data-store] applySync failed:",c)})},b.prototype.confirmMutation=function(e,t,r){const n=this,o={create:()=>q(n._name,e).then(()=>x(n._name,t)).then(()=>{delete n._pendingSnapshots[e],l(n.dom,"ln-store:confirmed",{store:n._name,record:t,tempId:e,action:"create"})}),update:()=>x(n._name,t).then(()=>{delete n._pendingSnapshots[e],l(n.dom,"ln-store:confirmed",{store:n._name,record:t,action:"update"})}),delete:()=>(delete n._pendingSnapshots[e],l(n.dom,"ln-store:confirmed",{store:n._name,record:null,action:"delete"}),Promise.resolve()),"bulk-delete":()=>(delete n._pendingSnapshots[e],l(n.dom,"ln-store:confirmed",{store:n._name,record:null,ids:e.split(","),action:"bulk-delete"}),Promise.resolve())};return o[r]?o[r]():Promise.resolve()},b.prototype.revertMutation=function(e,t,r){const n=this,o=r||`Server rejected ${t}`,i={create:()=>q(n._name,e).then(()=>{n.totalCount--,delete n._pendingSnapshots[e],l(n.dom,"ln-store:reverted",{store:n._name,record:null,action:"create",error:o})}),update:()=>{const c=n._pendingSnapshots[e];return c?x(n._name,c).then(()=>{delete n._pendingSnapshots[e],l(n.dom,"ln-store:reverted",{store:n._name,record:c,action:"update",error:o})}):Promise.resolve()},delete:()=>{const c=n._pendingSnapshots[e];return c?x(n._name,c).then(()=>{n.totalCount++,delete n._pendingSnapshots[e],l(n.dom,"ln-store:reverted",{store:n._name,record:c,action:"delete",error:o})}):Promise.resolve()},"bulk-delete":()=>{const c=n._pendingSnapshots[e];return!c||!c.length?Promise.resolve():X(n._name,c).then(()=>{n.totalCount+=c.length,delete n._pendingSnapshots[e],l(n.dom,"ln-store:reverted",{store:n._name,record:null,ids:e.split(","),action:"bulk-delete",error:o})})}};return i[t]?i[t]():Promise.resolve()},b.prototype.resolveConflict=function(e,t,r){const n=this._pendingSnapshots[e];return n?x(this._name,n).then(()=>{delete this._pendingSnapshots[e],l(this.dom,"ln-store:conflict",{store:this._name,local:n,remote:t,field_diffs:r||null})}):Promise.resolve()},b.prototype.forceSync=function(){N(this)},b.prototype.fullReload=function(){const e=this;return J(e._name).then(()=>{e.isLoaded=!1,e.lastSyncedAt=null,e.totalCount=0,N(e)})},b.prototype.destroy=function(){if(this._handlers){for(const[e,t]of Object.entries(this._handlers))this.dom.removeEventListener(`ln-store:request-${e}`,t);this._handlers=null}delete u[this._name],Object.keys(u).length===0&&K&&(document.removeEventListener("visibilitychange",K),K=null),delete this.dom[a],l(this.dom,"ln-store:destroyed",{store:this._name})};function ye(){return p().then(e=>{if(!e)return;const t=Array.from(e.objectStoreNames);return new Promise((r,n)=>{const o=e.transaction(t,"readwrite");t.forEach(i=>o.objectStore(i).clear()),o.oncomplete=()=>r(),o.onerror=()=>n(o.error)})}).then(()=>{Object.values(u).forEach(e=>{e.isLoaded=!1,e.isSyncing=!1,e.lastSyncedAt=null,e.totalCount=0})})}ee(s,a,b,"ln-data-store"),window[a].clearAll=ye,window[a].init=window[a],window[a].setStorageKey=G,typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.setStorageKey=G)})()})();
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const n=console.warn;console.warn=function(...o){typeof o[0]=="string"&&(o[0].startsWith("[ln-")||o[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||n.apply(console,o)}}const U={};function at(n,o){U[n]||(U[n]=document.querySelector('[data-ln-template="'+n+'"]'));const l=U[n];return l?l.content.cloneNode(!0):(console.warn("["+(o||"ln-core")+'] Template "'+n+'" not found'),null)}function C(n,o,l){n.dispatchEvent(new CustomEvent(o,{bubbles:!0,detail:l||{}}))}function H(n,o){if(!n||!o)return n;const l=n.querySelectorAll("[data-ln-field]");for(let s=0;s<l.length;s++){const u=l[s],d=u.getAttribute("data-ln-field");o[d]!=null&&(u.textContent=o[d])}const f=n.querySelectorAll("[data-ln-attr]");for(let s=0;s<f.length;s++){const u=f[s],d=u.getAttribute("data-ln-attr").split(",");for(let p=0;p<d.length;p++){const _=d[p].trim().split(":");if(_.length!==2)continue;const b=_[0].trim(),g=_[1].trim();o[g]!=null&&u.setAttribute(b,o[g])}}const m=n.querySelectorAll("[data-ln-show]");for(let s=0;s<m.length;s++){const u=m[s],d=u.getAttribute("data-ln-show");d in o&&u.classList.toggle("hidden",!o[d])}const h=n.querySelectorAll("[data-ln-class]");for(let s=0;s<h.length;s++){const u=h[s],d=u.getAttribute("data-ln-class").split(",");for(let p=0;p<d.length;p++){const _=d[p].trim().split(":");if(_.length!==2)continue;const b=_[0].trim(),g=_[1].trim();g in o&&u.classList.toggle(b,!!o[g])}}return n}function st(n,o){n.matches&&n.matches("[data-ln-form], [data-ln-fillable]")&&n.dispatchEvent(new CustomEvent("ln-fill",{detail:o??null,bubbles:!0}));const l=n.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let f=0;f<l.length;f++)l[f].dispatchEvent(new CustomEvent("ln-fill",{detail:o??null,bubbles:!0}));return n}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(n){if(!(!n.target.matches||!n.target.matches("[data-ln-fillable]")))if(n.detail)H(n.target,n.detail);else{const o=n.target.querySelectorAll("[data-ln-field]");for(let l=0;l<o.length;l++)o[l].textContent=""}})));function Q(n,o){if(!n||!o)return n;const l=document.createTreeWalker(n,NodeFilter.SHOW_TEXT);for(;l.nextNode();){const h=l.currentNode;h.textContent.indexOf("{{")!==-1&&(h.textContent=h.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(s,u){return o[u]!==void 0?o[u]:""}))}const f=function(h,s){return o[s]!==void 0?o[s]:""},m=Array.from(n.querySelectorAll("*"));n.nodeType===1&&m.push(n);for(let h=0;h<m.length;h++){const s=m[h],u=s.attributes;for(let d=0;d<u.length;d++){const p=u[d];p.value.indexOf("{{")!==-1&&s.setAttribute(p.name,p.value.replace(/\{\{\s*(\w+)\s*\}\}/g,f))}}return n}function lt(n,o,l,f,m,h){const s={};for(let d=0;d<n.children.length;d++){const p=n.children[d],_=p.getAttribute("data-ln-key");_&&(s[_]=p)}const u=document.createDocumentFragment();for(let d=0;d<o.length;d++){const p=o[d],_=String(f(p));let b=s[_];if(b)m(b,p,d);else{const g=at(l,h);if(!g||(Q(g,p),b=g.firstElementChild,!b))continue;b.setAttribute("data-ln-key",_),m(b,p,d)}u.appendChild(b)}n.textContent="",n.appendChild(u)}function V(n,o){if(!document.body){document.addEventListener("DOMContentLoaded",function(){V(n,o)}),console.warn("["+o+'] Script loaded before <body> — add "defer" to your <script> tag');return}n()}function F(n,o,l,f){if(n.nodeType!==1)return;const h=o.indexOf("[")!==-1||o.indexOf(".")!==-1||o.indexOf("#")!==-1?o:"["+o+"]",s=Array.from(n.querySelectorAll(h));n.matches&&n.matches(h)&&s.push(n);for(const u of s)u[l]||(u[l]=new f(u))}function ct(n,o,l,f,m={}){const h=m.extraAttributes||[],s=m.onAttributeChange||null,u=m.onInit||null;function d(p){const _=p||document.body;F(_,n,o,l),u&&u(_)}return V(function(){const p=new MutationObserver(function(b){for(let g=0;g<b.length;g++){const v=b[g];if(v.type==="childList"){for(let k=0;k<v.addedNodes.length;k++){const S=v.addedNodes[k];S.nodeType===1&&(F(S,n,o,l),u&&u(S))}for(let k=0;k<v.removedNodes.length;k++){const S=v.removedNodes[k];if(S.nodeType===1){const q=n.indexOf("[")!==-1||n.indexOf(".")!==-1||n.indexOf("#")!==-1?n:"["+n+"]",D=Array.from(S.querySelectorAll(q));S.matches&&S.matches(q)&&D.push(S);for(let R=0;R<D.length;R++){const T=D[R];if(!document.contains(T)){const M=T[o];M&&typeof M.destroy=="function"&&M.destroy()}}}}}else v.type==="attributes"&&(s&&v.target[o]?s(v.target,v.attributeName):(F(v.target,n,o,l),u&&u(v.target)))}});let _=[];if(n.indexOf("[")!==-1){const b=/\[([\w-]+)/g;let g;for(;(g=b.exec(n))!==null;)_.push(g[1])}else _.push(n);p.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:_.concat(h)})},f),window[o]=d,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){d(document.body)}):d(document.body),d}const J={};function ut(n,o){J[n]=o}function dt(n){return J[n]||{ingress:o=>o,egress:o=>o}}const X={};function Y(n,o){if(!n||typeof o!="object")return;const l=n.toLowerCase().split("-")[0];X[l]=o}function ht(n){if(!n)return null;const o=n.toLowerCase().split("-")[0];return X[o]||null}Y("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=ut,window.lnCore.getDataMapper=dt,window.lnCore.registerLocaleFallback=Y,window.lnCore.getLocaleFallback=ht,window.lnCore.fillTemplate=Q,window.lnCore.fill=H,window.lnCore.lnFill=st,window.lnCore.renderList=lt);function Z(n){return(n||"").replace(/^#/,"")}function W(n){const o=n===void 0?location.hash:n,l={},f=Z(o);if(!f)return l;const m=f.split("&");for(let h=0;h<m.length;h++){const s=m[h];if(!s)continue;const u=s.indexOf(":"),d=u>-1?s.slice(0,u):s,p=u>-1?s.slice(u+1):"";if(d)try{l[d]=decodeURIComponent(p)}catch{l[d]=p}}return l}function ft(n){if(!n)return null;const o=W();return n in o?o[n]:null}function mt(n,o){if(!n)return;const l=W();o==null?delete l[n]:l[n]=String(o);const m=Object.keys(l).map(function(h){const s=l[h];return s===""?h:h+":"+encodeURIComponent(s)}).join("&");Z(location.hash)!==m&&(location.hash=m)}function pt(n){return n.button===1||n.ctrlKey||n.metaKey||n.shiftKey?!1:(n.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=W,window.lnCore.hashGet=ft,window.lnCore.hashSet=mt,window.lnCore.hashLinkClick=pt);let P=null;async function tt(n){if(!n){P=null;return}try{const o=new TextEncoder,l=await crypto.subtle.digest("SHA-256",o.encode(n));P=await crypto.subtle.importKey("raw",l,{name:"AES-GCM"},!1,["encrypt","decrypt"])}catch(o){console.error("[ln-core/crypto] Key derivation failed:",o),P=null}}function B(){return P}async function yt(n,o=P){const l=o||P;if(!l||n===void 0||n===null)return n;try{const f=new TextEncoder,m=crypto.getRandomValues(new Uint8Array(12)),h=typeof n=="string"?n:JSON.stringify(n),s=await crypto.subtle.encrypt({name:"AES-GCM",iv:m},l,f.encode(h)),u=btoa(String.fromCharCode(...m)),d=btoa(String.fromCharCode(...new Uint8Array(s)));return{encrypted:!0,iv:u,data:d}}catch(f){return console.error("[ln-core/crypto] Encryption failed:",f),n}}async function _t(n,o=P){const l=o||P;if(!n||!n.encrypted||!l)return n;try{const f=new TextDecoder,m=Uint8Array.from(atob(n.iv),d=>d.charCodeAt(0)),h=Uint8Array.from(atob(n.data),d=>d.charCodeAt(0)),s=await crypto.subtle.decrypt({name:"AES-GCM",iv:m},l,h),u=f.decode(s);try{return JSON.parse(u)}catch{return u}}catch(f){return console.error("[ln-core/crypto] Decryption failed. Key may be incorrect:",f),{...n,decryptionError:!0}}}(function(){const n="data-ln-data-store",o="lnDataStore";if(window[o]!==void 0)return;const l="ln_app_cache",f="_meta",m="1.0";let h=null,s=null;const u={};function d(t){t&&t.name==="QuotaExceededError"&&C(document,"ln-data-store:quota-exceeded",{error:t})}function p(){const t={};for(const e of document.querySelectorAll(`[${n}]`)){const r=e.getAttribute(n);if(r){const a=e.getAttribute("data-ln-data-store-indexes")||"";t[r]={indexes:a.split(",").map(i=>i.trim()).filter(Boolean)}}}return t}function _(){return s||(s=new Promise(t=>{if(typeof indexedDB>"u")return console.warn("[ln-data-store] IndexedDB not available — falling back to in-memory store"),t(null);const e=p(),r=Object.keys(e),a=indexedDB.open(l);a.onerror=()=>{console.warn("[ln-data-store] IndexedDB open failed — falling back to in-memory store"),t(null)},a.onsuccess=i=>{const c=i.target.result,y=Array.from(c.objectStoreNames);if(!(!y.includes(f)||r.some(N=>!y.includes(N))))return b(c),h=c,t(c);const A=c.version;c.close();const x=indexedDB.open(l,A+1);x.onblocked=()=>{console.warn("[ln-data-store] Database upgrade blocked — waiting for other tabs to close connection")},x.onerror=()=>{console.warn("[ln-data-store] Database upgrade failed"),t(null)},x.onupgradeneeded=N=>{const O=N.target.result;O.objectStoreNames.contains(f)||O.createObjectStore(f,{keyPath:"key"});for(const $ of r)if(!O.objectStoreNames.contains($)){const Bt=O.createObjectStore($,{keyPath:"id"});for(const it of e[$].indexes)Bt.createIndex(it,it,{unique:!1})}},x.onsuccess=N=>{const O=N.target.result;b(O),h=O,t(O)}}}),s)}function b(t){t.onversionchange=()=>{t.close(),h=null,s=null}}function g(){return h?Promise.resolve(h):(s=null,_())}async function v(t){if(!B()||!t)return t;const e={...t},r=e.id,a=await yt(e);return!a||!a.encrypted?t:{id:r,encrypted:!0,iv:a.iv,data:a.data}}async function k(t){return!t||!t.encrypted||!B()?t:_t(t)}const S=(t,e)=>g().then(r=>r?r.transaction(t,e).objectStore(t):null);function L(t){return new Promise((e,r)=>{t.onsuccess=()=>e(t.result),t.onerror=()=>{d(t.error),r(t.error)}})}const q=t=>S(t,"readonly").then(e=>e?L(e.getAll()):[]).then(e=>B()?Promise.all(e.map(r=>k(r))):e),D=(t,e)=>S(t,"readonly").then(r=>r?L(r.get(e)):null).then(r=>r?k(r):null),R=(t,e)=>(B()?v(e):Promise.resolve(e)).then(a=>S(t,"readwrite").then(i=>i?L(i.put(a)):null)),T=(t,e)=>S(t,"readwrite").then(r=>r?L(r.delete(e)):null),M=t=>S(t,"readwrite").then(e=>e?L(e.clear()):null),j=t=>S(t,"readonly").then(e=>e?L(e.count()):0),gt=t=>S(f,"readonly").then(e=>e?L(e.get(t)):null),I=(t,e)=>S(f,"readwrite").then(r=>{if(r)return e.key=t,L(r.put(e))});function E(t){this.dom=t,this._name=t.getAttribute(n);const e=t.getAttribute("data-ln-data-store-stale"),r=parseInt(e,10);this._staleThreshold=e==="never"||e==="-1"?-1:isNaN(r)?300:r;const a=t.getAttribute("data-ln-data-store-search-fields")||"";return this._searchFields=a.split(",").map(i=>i.trim()).filter(Boolean),this._handlers=null,this.isLoaded=!1,this.isInitialized=!1,this.initializationError=null,this.hasCache=!1,this.isSyncing=!1,this.lastSyncedAt=null,this.totalCount=0,this.presenters=null,this._mutationChain=Promise.resolve(),u[this._name]=this,wt(this),this.ready=xt(this),this}function wt(t){t._handlers={create:e=>z(t,"create",e.detail,()=>Ct(t,e.detail)),update:e=>z(t,"update",e.detail,()=>bt(t,e.detail)),delete:e=>z(t,"delete",e.detail,()=>St(t,e.detail)),"bulk-delete":e=>z(t,"bulk-delete",e.detail,()=>At(t,e.detail)),"sync-failed":e=>{t.isSyncing=!1,C(t.dom,"ln-data-store:sync-error",{store:t._name,error:e.detail&&e.detail.error,status:e.detail&&e.detail.status})}};for(const[e,r]of Object.entries(t._handlers))t.dom.addEventListener(`ln-data-store:request-${e}`,r)}function z(t,e,r,a){const i=r&&r.requestId;return t._mutationChain=t._mutationChain.then(()=>t.ready).then(()=>{if(t.initializationError)throw t.initializationError;return a()}).catch(c=>vt(t,e,i,c)),t._mutationChain}function K(t){return j(t._name).then(e=>(t.totalCount=e,t.hasCache=!0,t.isLoaded=!0,I(t._name,{schema_version:m,last_synced_at:t.lastSyncedAt,has_cache:!0,record_count:e})))}function Ct(t,{tempId:e,data:r={},requestId:a}={}){const i={...r,id:e};return R(t._name,i).then(()=>K(t)).then(()=>{C(t.dom,"ln-data-store:created",{store:t._name,record:i,tempId:e,requestId:a})})}function bt(t,{id:e,data:r={},requestId:a}={}){return D(t._name,e).then(i=>{if(!i)throw new Error(`Record not found: ${e}`);const c={...i,...r},y=r.id;return(y!==void 0&&y!==e?Et(t._name,e,c):R(t._name,c)).then(()=>K(t)).then(()=>{C(t.dom,"ln-data-store:updated",{store:t._name,record:c,previous:i,requestId:a})})})}function St(t,{id:e,requestId:r}={}){return D(t._name,e).then(a=>{if(!a){C(t.dom,"ln-data-store:deleted",{store:t._name,id:e,requestId:r,missing:!0});return}return T(t._name,e).then(()=>K(t)).then(()=>{C(t.dom,"ln-data-store:deleted",{store:t._name,id:e,requestId:r})})})}function At(t,{ids:e=[],requestId:r}={}){return e.length?Promise.all(e.map(a=>D(t._name,a))).then(a=>{const i=a.filter(Boolean).map(c=>c.id);return rt(t._name,i).then(()=>K(t)).then(()=>{C(t.dom,"ln-data-store:deleted",{store:t._name,ids:i,requestId:r})})}):(C(t.dom,"ln-data-store:deleted",{store:t._name,ids:[],requestId:r}),Promise.resolve())}function vt(t,e,r,a){console.error("[ln-data-store] "+e+" failed:",a),C(t.dom,"ln-data-store:mutation-error",{store:t._name,action:e,requestId:r,error:a})}function xt(t){return _().then(e=>{if(!e)throw new Error("IndexedDB is unavailable");return gt(t._name)}).then(e=>{if(t.initializationError=null,e&&e.schema_version===m)t.lastSyncedAt=e.last_synced_at||null,t.totalCount=e.record_count||0,t.hasCache=e.has_cache===!0||t.totalCount>0,t.hasCache&&(t.isLoaded=!0,C(t.dom,"ln-data-store:ready",{store:t._name,count:t.totalCount,source:"cache"})),t.isInitialized=!0,C(t.dom,"ln-data-store:initialized",{store:t._name,hasCache:t.hasCache,lastSyncedAt:t.lastSyncedAt,count:t.totalCount});else{if(e&&e.schema_version!==m)return M(t._name).then(()=>I(t._name,{schema_version:m,last_synced_at:null,has_cache:!1,record_count:0})).then(()=>{t.isInitialized=!0,t.hasCache=!1,C(t.dom,"ln-data-store:initialized",{store:t._name,hasCache:!1,lastSyncedAt:null,count:0})});t.isInitialized=!0,t.hasCache=!1,C(t.dom,"ln-data-store:initialized",{store:t._name,hasCache:!1,lastSyncedAt:null,count:0})}}).catch(e=>(t.isInitialized=!0,t.isLoaded=!1,t.hasCache=!1,t.isSyncing=!1,t.initializationError=e,C(t.dom,"ln-data-store:initialization-error",{store:t._name,error:e}),{ok:!1,error:e}))}function et(t){t.isSyncing=!0,C(t.dom,"ln-data-store:request-remote-sync",{since:t.lastSyncedAt})}function nt(t,e){return g().then(r=>r?(B()?Promise.all(e.map(i=>v(i))):Promise.resolve(e)).then(i=>new Promise((c,y)=>{const w=r.transaction(t,"readwrite"),A=w.objectStore(t);i.forEach(x=>A.put(x)),w.oncomplete=()=>c(),w.onerror=()=>{d(w.error),y(w.error)}})):void 0)}function rt(t,e){return g().then(r=>{if(r)return new Promise((a,i)=>{const c=r.transaction(t,"readwrite"),y=c.objectStore(t);e.forEach(w=>y.delete(w)),c.oncomplete=()=>a(),c.onerror=()=>i(c.error)})})}function Et(t,e,r){return(B()?v(r):Promise.resolve(r)).then(i=>g().then(c=>{if(c)return new Promise((y,w)=>{const A=c.transaction(t,"readwrite"),x=A.objectStore(t);x.put(i),x.delete(e),A.oncomplete=()=>y(),A.onerror=()=>{d(A.error),w(A.error)}})}))}const kt=new Intl.Collator(void 0,{numeric:!0,sensitivity:"base"});function Lt(t,e){if(!e||!e.field)return t;const{field:r,direction:a}=e,i=a==="desc";return[...t].sort((c,y)=>{const w=c[r],A=y[r];if(w==null&&A==null)return 0;if(w==null)return i?1:-1;if(A==null)return i?-1:1;const x=typeof w=="string"&&typeof A=="string"?kt.compare(w,A):w<A?-1:w>A?1:0;return i?-x:x})}function ot(t,e){if(!e)return t;const r=Object.keys(e).filter(a=>Array.isArray(e[a])&&e[a].length>0);return r.length?t.filter(a=>r.every(i=>e[i].map(String).includes(String(a[i])))):t}function Pt(t,e,r){if(!e||!r||!r.length)return t;const a=e.toLowerCase();return t.filter(i=>r.some(c=>{const y=i[c];return y!=null&&String(y).toLowerCase().includes(a)}))}function Dt(t,e,r){if(!t.length)return 0;if(r==="count")return t.length;const a=t.map(c=>parseFloat(c[e])).filter(c=>!isNaN(c)),i=a.reduce((c,y)=>c+y,0);return r==="sum"?i:r==="avg"&&a.length?i/a.length:0}function G(t,e){if(!t.presenters||!t.presenters.computed)return e;const r=t.presenters.computed;return e.map(a=>{const i={...a};for(const[c,y]of Object.entries(r))try{i[c]=y(a)}catch(w){console.error(`[ln-data-store] Decorator computed field failed for ${c}`,w)}return i})}E.prototype.getAll=function(t={}){const e=this;return q(e._name).then(r=>{const a=r.length;t.filters&&(r=ot(r,t.filters)),t.search&&(r=Pt(r,t.search,e._searchFields));const i=r.length;if(t.sort&&(r=Lt(r,t.sort)),t.offset||t.limit){const c=t.offset||0,y=t.limit||r.length;r=r.slice(c,c+y)}return{data:G(e,r),total:a,filtered:i}})},E.prototype.getById=function(t){return D(this._name,t).then(e=>e?G(this,[e])[0]:null)},E.prototype.count=function(t){return t?q(this._name).then(e=>ot(e,t).length):j(this._name)},E.prototype.aggregate=function(t,e){return q(this._name).then(r=>Dt(r,t,e))},E.prototype.setPresenters=function(t){this.presenters=t},E.prototype.applySync=function(t,e,r,a){a=a||{};const i=this;let c=Promise.resolve();return t.length>0&&(c=c.then(()=>nt(i._name,t))),e.length>0&&(c=c.then(()=>rt(i._name,e))),c.then(()=>j(i._name)).then(y=>(i.totalCount=a.total!==void 0?a.total:y,i.hasCache=!0,I(i._name,{schema_version:m,last_synced_at:r,has_cache:!0,record_count:i.totalCount}))).then(()=>{const y=!i.isLoaded;i.isLoaded=!0,i.isSyncing=!1,i.lastSyncedAt=r,y?(C(i.dom,"ln-data-store:loaded",{store:i._name,count:i.totalCount,meta:a}),C(i.dom,"ln-data-store:ready",{store:i._name,count:i.totalCount,source:"server",meta:a})):C(i.dom,"ln-data-store:synced",{store:i._name,added:t.length,deleted:e.length,changed:!0,meta:a})}).catch(y=>{i.isSyncing=!1,console.error("[ln-data-store] applySync failed:",y)})},E.prototype.applyQuery=function(t,e){e=e||{};const r=this;let a=Promise.resolve();return t.length>0&&(a=a.then(()=>nt(r._name,t))),a.then(()=>j(r._name)).then(i=>(r.totalCount=e.total!==void 0?e.total:i,G(r,t))).catch(i=>(console.error("[ln-data-store] applyQuery failed:",i),[]))},E.prototype.forceSync=function(){this.isSyncing||et(this)},E.prototype.fullReload=function(){const t=this;return M(t._name).then(()=>I(t._name,{schema_version:m,last_synced_at:null,has_cache:!1,record_count:0})).then(()=>{t.isLoaded=!1,t.hasCache=!1,t.lastSyncedAt=null,t.totalCount=0,et(t)})},E.prototype.destroy=function(){if(this._handlers){for(const[t,e]of Object.entries(this._handlers))this.dom.removeEventListener(`ln-data-store:request-${t}`,e);this._handlers=null}delete u[this._name],delete this.dom[o],C(this.dom,"ln-data-store:destroyed",{store:this._name})};function Ot(){return g().then(t=>{if(!t)return;const e=Array.from(t.objectStoreNames);return new Promise((r,a)=>{const i=t.transaction(e,"readwrite");e.forEach(c=>i.objectStore(c).clear()),i.oncomplete=()=>r(),i.onerror=()=>a(i.error)})}).then(()=>{Object.values(u).forEach(t=>{t.isLoaded=!1,t.isInitialized=!1,t.initializationError=null,t.hasCache=!1,t.isSyncing=!1,t.lastSyncedAt=null,t.totalCount=0})})}ct(n,o,E,"ln-data-store"),window[o].clearAll=Ot,window[o].init=window[o],window[o].setStorageKey=tt,typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.setStorageKey=tt)})()})();
|