@livenetworks/ashlar 1.5.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.
Files changed (217) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +92 -2
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -56
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -25
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +13 -3
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +36 -23
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +11 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +3 -5
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +2 -0
  198. package/scss/config/mixins/_ln-table.scss +41 -2
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +85 -60
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +27 -9
  209. package/scss/ln-ashlar.scss +7 -1
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1087
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -156
@@ -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 managing optimistic mutations with automatic snapshots for transaction safety.
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
- ## 🛠️ JS API (On the element)
22
-
23
- Access the database layer directly via the `lnDataStore` property on the store container:
24
-
25
- ```javascript
26
- const store = document.querySelector('[data-ln-data-store="documents"]');
27
-
28
- // 1. Queries (returns Promise)
29
- const { data, total, filtered } = await store.lnDataStore.getAll({
30
- sort: { field: 'updated_at', direction: 'desc' },
31
- filters: { status: ['Approved'] },
32
- search: 'ISO 27001',
33
- limit: 50
34
- });
35
-
36
- const doc = await store.lnDataStore.getById(42);
37
-
38
- // 2. Decorators / Computed Fields Configuration
39
- store.lnDataStore.setPresenters({
40
- computed: {
41
- size_display: record => (record.file_size / 1024).toFixed(1) + ' MB'
42
- }
43
- });
44
- ```
45
-
46
- ---
47
-
48
- ## ⚡ DOM Events
49
-
50
- ### Commands (Dispatched TO the store)
51
-
52
- *Never invoke write methods directly.* Route all mutations through DOM commands:
53
-
54
- ```javascript
55
- // Create optimistically
56
- store.dispatchEvent(new CustomEvent('ln-store:request-create', {
57
- detail: { data: { title: 'New Document', status: 'Draft' } }
58
- }));
59
-
60
- // Update with version locks
61
- store.dispatchEvent(new CustomEvent('ln-store:request-update', {
62
- detail: { id: 42, data: { title: 'Updated title' }, expected_version: 3 }
63
- }));
64
- ```
65
-
66
- ### Remote Request Events (Bubbles UP to the Coordinator)
67
-
68
- *The Coordinator catches these events to run transport sync and mutations:*
69
-
70
- * `ln-store:request-remote-sync` (detail: `{ since }`)
71
- * `ln-store:request-remote-create` (detail: `{ tempId, data }`)
72
- * `ln-store:request-remote-update` (detail: `{ id, data, expected_version }`)
73
- * `ln-store:request-remote-delete` (detail: `{ id }`)
74
- * `ln-store:request-remote-bulk-delete` (detail: `{ ids }`)
75
-
76
- ---
77
-
78
- ## 🚀 Public Synchronization APIs (Invoked by the Coordinator)
79
-
80
- Once the network connector returns backend payloads, the Coordinator feeds the results back to the store using these public methods:
81
-
82
- ```javascript
83
- // Feed delta updates or initial load data
84
- store.lnDataStore.applySync(upsertedRecords, deletedIds, syncedAt);
85
-
86
- // Confirm an optimistic transaction (swaps temp IDs, clears pending flag)
87
- store.lnDataStore.confirmMutation(tempIdOrId, serverRecord, action);
88
-
89
- // Revert an optimistic transaction (restores snapshot on error)
90
- store.lnDataStore.revertMutation(tempIdOrId, action, errorMessage);
91
-
92
- // Trigger conflict merge flow
93
- store.lnDataStore.resolveConflict(id, remoteRecord, fieldDiffs);
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)})()})();