@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.
Files changed (219) 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 +189 -99
  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 -55
  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 -0
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +14 -5
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +68 -1
  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 +48 -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 +47 -0
  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 +4 -1
  198. package/scss/config/mixins/_ln-table.scss +51 -93
  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 +232 -38
  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 +39 -0
  209. package/scss/ln-ashlar.scss +8 -2
  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 -1103
  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 -168
  218. package/scss/components/_data-table.scss +0 -83
  219. package/scss/config/mixins/_data-table.scss +0 -346
@@ -0,0 +1,213 @@
1
+ // ─── Windowed Sparse Cache ─────────────────────────────────
2
+ //
3
+ // DOM-agnostic sliding-window data engine for server-side virtualization.
4
+ // Owns a sparse cache keyed by logical index, logical/grand totals, request
5
+ // dedup, a stale-guard via queryGen, LRU eviction to a bounded cap, and a
6
+ // debounced fetch trigger. Transport-agnostic: the consumer supplies
7
+ // requestPage() (kick a fetch — any transport) and onChange() (cache mutated,
8
+ // re-render). Render clients pull rows via get()/has() and read logicalTotal
9
+ // for spacer geometry; they hold no cache of their own. Shared by the ln-table
10
+ // and ln-list windowed paths; a future ln-data-store windowed mode plugs in by
11
+ // making requestPage resolve getAll({offset,limit}) then call ingest().
12
+
13
+ export function createWindowCache(config) {
14
+ config = config || {};
15
+ let windowSize = config.windowSize > 0 ? config.windowSize : 1000;
16
+ let pageSize = config.pageSize > 0 ? config.pageSize : 200;
17
+ let threshold = config.threshold != null ? config.threshold : 25;
18
+ let fetchDebounce = config.fetchDebounce != null ? config.fetchDebounce : 120;
19
+ const requestPage = typeof config.requestPage === 'function' ? config.requestPage : function () {};
20
+ const onChange = typeof config.onChange === 'function' ? config.onChange : function () {};
21
+
22
+ const map = new Map(); // logicalIndex → record
23
+ const touch = new Map(); // logicalIndex → seq (LRU stamp)
24
+ const inflight = new Set(); // offsets currently requested (dedup)
25
+ let logicalTotal = 0;
26
+ let grandTotal = 0;
27
+ let queryGen = 0;
28
+ let query = { sort: null, filters: {}, search: '' };
29
+ let debounceId = null;
30
+ let seq = 0;
31
+ let lastRangeStart = 0; // last ensure() visible range — revalidate() re-targets here, not page 0
32
+ let lastRangeEnd = 0;
33
+ let pendingSwap = false; // set by invalidate()/revalidate(), consumed by the new generation's first ingest()
34
+
35
+ function stamp(i) { touch.set(i, ++seq); }
36
+
37
+ function hasActiveQuery() {
38
+ return !!(query && (query.search || (query.filters && Object.keys(query.filters).length)));
39
+ }
40
+
41
+ function evict() {
42
+ if (map.size <= windowSize) return;
43
+ // LRU: drop least-recently touched/requested rows first
44
+ const keys = Array.from(map.keys()).sort(function (a, b) {
45
+ return (touch.get(a) || 0) - (touch.get(b) || 0);
46
+ });
47
+ let i = 0;
48
+ while (map.size > windowSize && i < keys.length) {
49
+ map.delete(keys[i]);
50
+ touch.delete(keys[i]);
51
+ i++;
52
+ }
53
+ }
54
+
55
+ function fire(offset, limit) {
56
+ inflight.add(offset);
57
+ requestPage(query, offset, limit);
58
+ }
59
+
60
+ return {
61
+ get: function (i) { return map.get(i); },
62
+ has: function (i) { return map.has(i); },
63
+ peek: function () { return map.size ? map.values().next().value : undefined; },
64
+
65
+ get logicalTotal() { return logicalTotal; },
66
+ get grandTotal() { return grandTotal; },
67
+ get queryGen() { return queryGen; },
68
+ get size() { return map.size; },
69
+
70
+ // Render client hands its visible logical range; stamps in-range resident
71
+ // rows as freshly used, then checks if any page in range (padded by threshold)
72
+ // is missing from cache and needs to be fetched (page-aligned).
73
+ ensure: function (startRow, endRow) {
74
+ clearTimeout(debounceId);
75
+ lastRangeStart = startRow;
76
+ lastRangeEnd = endRow;
77
+ for (let i = startRow; i < endRow; i++) {
78
+ if (map.has(i)) stamp(i);
79
+ }
80
+ if (logicalTotal <= 0) return;
81
+
82
+ const checkStart = Math.max(0, startRow - threshold);
83
+ const checkEnd = Math.min(logicalTotal, endRow + threshold);
84
+
85
+ const firstPage = Math.floor(checkStart / pageSize);
86
+ const lastPage = Math.floor(Math.max(0, checkEnd - 1) / pageSize);
87
+
88
+ let targetPageOffset = -1;
89
+ let targetPageLimit = pageSize;
90
+
91
+ for (let p = firstPage; p <= lastPage; p++) {
92
+ const pOffset = p * pageSize;
93
+ const pLimit = Math.min(pageSize, logicalTotal - pOffset);
94
+
95
+ let hasMissing = false;
96
+ for (let i = pOffset; i < pOffset + pLimit; i++) {
97
+ if (!map.has(i)) {
98
+ hasMissing = true;
99
+ break;
100
+ }
101
+ }
102
+
103
+ if (hasMissing && !inflight.has(pOffset)) {
104
+ targetPageOffset = pOffset;
105
+ targetPageLimit = pLimit;
106
+ break;
107
+ }
108
+ }
109
+
110
+ if (targetPageOffset === -1) return;
111
+
112
+ debounceId = setTimeout(function () {
113
+ fire(targetPageOffset, targetPageLimit);
114
+ }, fetchDebounce);
115
+ },
116
+
117
+ // Splice a fetched page. Stale (superseded-query) responses are dropped.
118
+ // Out-of-order pages splice at their own offset, so order is irrelevant.
119
+ ingest: function (detail) {
120
+ detail = detail || {};
121
+ if (detail.queryGen != null && detail.queryGen !== queryGen) return;
122
+
123
+ // First response of a new generation: drop the stale rows now, at the
124
+ // swap moment, not at invalidate()/revalidate() time — stale-while-revalidate.
125
+ if (pendingSwap) {
126
+ map.clear();
127
+ touch.clear();
128
+ pendingSwap = false;
129
+ }
130
+
131
+ grandTotal = detail.total != null ? detail.total : grandTotal;
132
+ logicalTotal = detail.filtered != null
133
+ ? detail.filtered
134
+ : (detail.data ? detail.data.length : logicalTotal);
135
+
136
+ const offset = detail.offset || 0;
137
+ const rows = detail.data || [];
138
+ for (let i = 0; i < rows.length; i++) {
139
+ map.set(offset + i, rows[i]);
140
+ stamp(offset + i);
141
+ }
142
+ inflight.delete(offset);
143
+ evict();
144
+ onChange();
145
+ },
146
+
147
+ // First load: fetch page 0 at the current generation (no bump).
148
+ requestInitial: function (q) {
149
+ if (q) query = q;
150
+ fire(0, pageSize);
151
+ },
152
+
153
+ // Query change: new generation, stale rows stay visible until the first
154
+ // response of the new generation lands in ingest() — no blanking, no
155
+ // placeholder flash (ln-table--loading is the refresh affordance).
156
+ invalidate: function (q) {
157
+ queryGen++;
158
+ inflight.clear();
159
+ clearTimeout(debounceId);
160
+ if (q) query = q;
161
+ pendingSwap = true;
162
+ fire(0, pageSize);
163
+ },
164
+
165
+ // Post-mutation refresh of a windowed view: same stale-while-revalidate
166
+ // swap as invalidate(), but re-requests the page at the CURRENT scroll
167
+ // position instead of jumping back to page 0.
168
+ revalidate: function () {
169
+ queryGen++;
170
+ inflight.clear();
171
+ clearTimeout(debounceId);
172
+ pendingSwap = true;
173
+ const offset = Math.max(0, Math.floor(lastRangeStart / pageSize) * pageSize);
174
+ const limit = logicalTotal > 0 ? Math.min(pageSize, Math.max(1, logicalTotal - offset)) : pageSize;
175
+ fire(offset, limit);
176
+ },
177
+
178
+ // Failed page fetch: release the offset so the next ensure() (scroll,
179
+ // filter, resize) can re-request it. No onChange(), no auto-retry.
180
+ release: function (offset) {
181
+ inflight.delete(offset);
182
+ },
183
+
184
+ destroy: function () {
185
+ clearTimeout(debounceId);
186
+ map.clear();
187
+ touch.clear();
188
+ inflight.clear();
189
+ },
190
+
191
+ configure: function (partial) {
192
+ partial = partial || {};
193
+ let geometryChanged = false;
194
+ if (partial.windowSize != null && partial.windowSize > 0 && partial.windowSize !== windowSize) {
195
+ const shrank = partial.windowSize < windowSize;
196
+ windowSize = partial.windowSize;
197
+ if (shrank) evict();
198
+ geometryChanged = true;
199
+ }
200
+ if (partial.pageSize != null && partial.pageSize > 0) pageSize = partial.pageSize;
201
+ if (partial.threshold != null && partial.threshold >= 0) threshold = partial.threshold;
202
+ if (partial.fetchDebounce != null && partial.fetchDebounce >= 0) fetchDebounce = partial.fetchDebounce;
203
+ if (geometryChanged) onChange();
204
+ },
205
+
206
+ setGrandTotal: function (n) {
207
+ if (n == null || isNaN(n) || n < 0) return;
208
+ grandTotal = n;
209
+ if (!hasActiveQuery()) logicalTotal = n;
210
+ onChange();
211
+ }
212
+ };
213
+ }
@@ -1,156 +1,177 @@
1
- # `data-ln-couchdb-connector`
2
-
3
- A zero-dependency, Local-First sync transport component that implements the Transport Gateway pattern of `ln-ashlar` for CouchDB and Sync Gateway instances.
4
-
5
- This component encapsulates all connection parameters (CouchDB base URL, database name, auth credentials, headers) and provides a declarative, event-driven, or programmatic way to talk to any CouchDB-compatible backend using standard Changes Feed (`_changes`) protocols. It isolates networking concerns completely, making your cache store (`data-ln-data-store`) and visual presentation layers fully network-agnostic.
6
-
7
- ---
8
-
9
- ## Declarative DOM Setup
10
-
11
- Place the connector inside your parent coordinator element alongside your store:
12
-
13
- ```html
14
- <div data-ln-data-coordinator="tasks">
15
- <!-- Storage Layer Cache (Blind to networking) -->
16
- <div data-ln-data-store
17
- data-ln-store-indexes="due_date,priority">
18
- </div>
19
-
20
- <!-- Transport Gateway (CouchDB Connector) -->
21
- <div data-ln-couchdb-connector
22
- data-ln-couchdb-url="https://couch.livenetworks.com"
23
- data-ln-couchdb-db="tasks"
24
- data-ln-couchdb-auth="Basic dXNlcjpwYXNz">
25
- </div>
26
- </div>
27
- ```
28
-
29
- ---
30
-
31
- ## Attributes
32
-
33
- All attributes are dynamically observed. Any runtime changes to these attributes instantly update the connector's internal configurations.
34
-
35
- | Attribute | Category | Description |
36
- |-----------|----------|-------------|
37
- | `data-ln-couchdb-connector` | Selector | Creates the component instance. |
38
- | `data-ln-couchdb-url` | Connection | The base URL of the CouchDB Server or Sync Gateway (e.g. `https://couch.example.com`). Fallbacks: `data-ln-couchdb-connector-url`, `data-ln-api-base-url`, `data-ln-rest-base-url`. |
39
- | `data-ln-couchdb-db` | Connection | Database name endpoint (e.g. `tasks`). Fallbacks: `data-ln-couchdb-connector-db`, `data-ln-api-path`, `data-ln-rest-path`. |
40
- | `data-ln-couchdb-auth` | Credentials | Authorization header value (e.g. `Basic dXNlcjpwYXNz` or standard token). |
41
- | `data-ln-couchdb-headers` | Credentials | JSON-formatted string of custom request headers. |
42
-
43
- ---
44
-
45
- ## JavaScript API Methods
46
-
47
- You can access the instance methods directly on the element via the `.lnCouchDbConnector` or `.lnConnector` properties:
48
-
49
- ```javascript
50
- const connectorEl = document.querySelector('[data-ln-couchdb-connector]');
51
- const connector = connectorEl.lnCouchDbConnector; // or connectorEl.lnConnector
52
-
53
- // 1. Fetch changed records since a sequence ID (CouchDB delta protocol)
54
- connector.fetchDelta("1-g1AAA...")
55
- .then(payload => {
56
- console.log('Upserted docs:', payload.data);
57
- console.log('Deleted IDs:', payload.deleted);
58
- console.log('New sync seq:', payload.synced_at);
59
- });
60
-
61
- // 2. Create a new document (maps payload to CouchDB _id if id is provided)
62
- connector.create({ title: 'New task', priority: 'High' })
63
- .then(doc => console.log('Created CouchDB document:', doc));
64
-
65
- // 3. Update an existing document (handles _rev fetching automatically if omitted)
66
- connector.update("doc_123", { title: 'Updated Task Title' })
67
- .then(doc => console.log('Updated CouchDB document:', doc));
68
-
69
- // 4. Delete a document (handles _rev fetching automatically if omitted)
70
- connector.delete("doc_123")
71
- .then(res => console.log('Deleted successfully:', res));
72
-
73
- // 5. Bulk Delete records (queries all revisions first, then performs _bulk_docs deletion)
74
- connector.bulkDelete(["doc_17", "doc_23"])
75
- .then(res => console.log('Bulk deleted result:', res));
76
- ```
77
-
78
- ---
79
-
80
- ## DOM Events
81
-
82
- ### Commands (Dispatched TO the connector)
83
-
84
- You can trigger mutations and fetches asynchronously by dispatching standard events directly on the connector DOM element. All events are supported in `ln-couchdb-connector`, `ln-api-connector`, and `ln-rest-connector` namespaces for 100% backward compatibility.
85
-
86
- | Event | `detail` Payload | Description |
87
- |-------|------------------|-------------|
88
- | `ln-couchdb-connector:request-sync` | `{ since }` | Triggers a Changes Feed fetch request. |
89
- | `ln-couchdb-connector:request-create` | `{ data, tempId }` | Triggers a creation request. |
90
- | `ln-couchdb-connector:request-update` | `{ id, data, expected_version }` | Triggers a PUT update (expected_version maps to CouchDB's revision). |
91
- | `ln-couchdb-connector:request-delete` | `{ id, rev }` | Triggers a deletion request. |
92
- | `ln-couchdb-connector:request-bulk-delete` | `{ ids }` | Triggers a bulk-deletion request. |
93
-
94
- ### Notifications (Emitted BY the connector)
95
-
96
- The connector dispatches bubbles-enabled events to notify parent coordinators of response states.
97
-
98
- | Event | `detail` Payload | Description |
99
- |-------|------------------|-------------|
100
- | `ln-couchdb-connector:fetched` | `{ data, since }` | Dispatched upon successful fetch completion. |
101
- | `ln-couchdb-connector:created` | `{ record, tempId }` | Dispatched upon successful server record confirmation. |
102
- | `ln-couchdb-connector:updated` | `{ record, id }` | Dispatched upon successful server update confirmation. |
103
- | `ln-couchdb-connector:deleted` | `{ response, id }` | Dispatched upon successful server deletion. |
104
- | `ln-couchdb-connector:bulk-deleted`| `{ response, ids }` | Dispatched upon successful server bulk deletion. |
105
- | `ln-couchdb-connector:error` | `{ action, error, status, ... }` | Dispatched on failures. Includes `conflictData` if status is 409. |
106
- | `ln-couchdb-connector:config-changed` | `{ url, db, auth, headers }` | Dispatched when configuration attributes are mutated. |
107
-
108
- ---
109
-
110
- ## 3-Tier Integration Architecture (Coordinator Example)
111
-
112
- Here is how a parent Coordinator links a Cache Store and the CouchDB Connector using standard DOM events:
113
-
114
- ```javascript
115
- (function () {
116
- const parent = document.querySelector('[data-ln-data-coordinator="tasks"]');
117
- const storeEl = parent.querySelector('[data-ln-data-store]');
118
- const connectorEl = parent.querySelector('[data-ln-couchdb-connector]');
119
-
120
- if (!storeEl || !connectorEl) return;
121
-
122
- // 1. Storage needs remote sync -> forward request to CouchDB Connector
123
- storeEl.addEventListener('ln-store:request-remote-sync', function (e) {
124
- connectorEl.dispatchEvent(new CustomEvent('ln-couchdb-connector:request-sync', {
125
- detail: { since: e.detail.since }
126
- }));
127
- });
128
-
129
- // 2. CouchDB Connector finishes changes feed sync -> feed delta back to Storage
130
- connectorEl.addEventListener('ln-couchdb-connector:fetched', function (e) {
131
- const payload = e.detail.data;
132
- // payload: { data: Array, deleted: Array, synced_at: String }
133
- storeEl.lnStore.applySync(payload.data, payload.deleted || [], payload.synced_at);
134
- });
135
-
136
- // 3. Storage requests remote creation -> forward payload to CouchDB Connector
137
- storeEl.addEventListener('ln-store:request-remote-create', function (e) {
138
- connectorEl.dispatchEvent(new CustomEvent('ln-couchdb-connector:request-create', {
139
- detail: { data: e.detail.data, tempId: e.detail.tempId }
140
- }));
141
- });
142
-
143
- // 4. CouchDB Connector confirms creation -> swap optimistic temp records in Storage
144
- connectorEl.addEventListener('ln-couchdb-connector:created', function (e) {
145
- storeEl.lnStore.confirmMutation(e.detail.tempId, e.detail.record, 'create');
146
- });
147
-
148
- // 5. In case of network errors -> trigger rollback in Cache Store
149
- connectorEl.addEventListener('ln-couchdb-connector:error', function (e) {
150
- if (e.detail.action === 'create') {
151
- storeEl.lnStore.revertMutation(e.detail.tempId, 'create', e.detail.error);
152
- }
153
- // ... handle updates and deletions similarly
154
- });
155
- })();
156
- ```
1
+ # `data-ln-couchdb-connector`
2
+
3
+ A zero-dependency, Local-First sync transport component that implements the Transport Gateway pattern of `ln-ashlar` for CouchDB and Sync Gateway instances.
4
+
5
+ This component encapsulates all connection parameters (CouchDB base URL, database name, auth credentials, headers) and provides a declarative, event-driven, or programmatic way to talk to any CouchDB-compatible backend using standard Changes Feed (`_changes`) protocols. It isolates networking concerns completely, making your cache store (`data-ln-data-store`) and visual presentation layers fully network-agnostic.
6
+
7
+ ---
8
+
9
+ ## Declarative DOM Setup
10
+
11
+ Place the connector inside your parent coordinator element alongside your store:
12
+
13
+ ```html
14
+ <div data-ln-data-coordinator="tasks">
15
+ <!-- Storage Layer Cache (Blind to networking) -->
16
+ <div data-ln-data-store
17
+ data-ln-data-store-indexes="due_date,priority">
18
+ </div>
19
+
20
+ <!-- Transport Gateway (CouchDB Connector) -->
21
+ <div data-ln-couchdb-connector
22
+ data-ln-couchdb-url="https://couch.livenetworks.com"
23
+ data-ln-couchdb-db="tasks"
24
+ data-ln-couchdb-auth="Basic dXNlcjpwYXNz">
25
+ </div>
26
+ </div>
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Attributes
32
+
33
+ All attributes are dynamically observed. Any runtime changes to these attributes instantly update the connector's internal configurations.
34
+
35
+ | Attribute | Category | Description |
36
+ |-----------|----------|-------------|
37
+ | `data-ln-couchdb-connector` | Selector | Creates the component instance. |
38
+ | `data-ln-couchdb-url` | Connection | The base URL of the CouchDB Server or Sync Gateway (e.g. `https://couch.example.com`). Fallbacks: `data-ln-couchdb-connector-url`, `data-ln-api-base-url`, `data-ln-rest-base-url`. |
39
+ | `data-ln-couchdb-db` | Connection | Database name endpoint (e.g. `tasks`). Fallbacks: `data-ln-couchdb-connector-db`, `data-ln-api-path`, `data-ln-rest-path`. |
40
+ | `data-ln-couchdb-auth` | Credentials | Authorization header value (e.g. `Basic dXNlcjpwYXNz` or standard token). |
41
+ | `data-ln-couchdb-headers` | Credentials | JSON-formatted string of custom request headers. |
42
+
43
+ ---
44
+
45
+ ## JavaScript API Methods
46
+
47
+ You can access the instance methods directly on the element via the `.lnCouchDbConnector` or `.lnConnector` properties:
48
+
49
+ ```javascript
50
+ const connectorEl = document.querySelector('[data-ln-couchdb-connector]');
51
+ const connector = connectorEl.lnCouchDbConnector; // or connectorEl.lnConnector
52
+
53
+ // 1. Fetch changed records since a sequence ID (CouchDB delta protocol)
54
+ connector.fetchDelta("1-g1AAA...")
55
+ .then(payload => {
56
+ console.log('Upserted docs:', payload.data);
57
+ console.log('Deleted IDs:', payload.deleted);
58
+ console.log('New sync seq:', payload.synced_at);
59
+ });
60
+
61
+ // 2. Create a new document (maps payload to CouchDB _id if id is provided)
62
+ connector.create({ title: 'New task', priority: 'High' })
63
+ .then(doc => console.log('Created CouchDB document:', doc));
64
+
65
+ // 3. Update an existing document (handles _rev fetching automatically if omitted)
66
+ connector.update("doc_123", { title: 'Updated Task Title' })
67
+ .then(doc => console.log('Updated CouchDB document:', doc));
68
+
69
+ // 4. Delete a document (handles _rev fetching automatically if omitted)
70
+ connector.delete("doc_123")
71
+ .then(res => console.log('Deleted successfully:', res));
72
+
73
+ // 5. Bulk Delete records (queries all revisions first, then performs _bulk_docs deletion)
74
+ connector.bulkDelete(["doc_17", "doc_23"])
75
+ .then(res => console.log('Bulk deleted result:', res));
76
+ ```
77
+
78
+ ---
79
+
80
+ ## DOM Events
81
+
82
+ ### Commands (Dispatched TO the connector)
83
+
84
+ You can trigger mutations and fetches asynchronously by dispatching standard events directly on the connector DOM element. All events are supported in `ln-couchdb-connector`, `ln-api-connector`, and `ln-rest-connector` namespaces for 100% backward compatibility.
85
+
86
+ | Event | `detail` Payload | Description |
87
+ |-------|------------------|-------------|
88
+ | `ln-couchdb-connector:request-sync` | `{ since }` | Triggers a Changes Feed fetch request. |
89
+ | `ln-couchdb-connector:request-create` | `{ data, tempId, idempotencyKey? }` | Triggers a creation request. |
90
+ | `ln-couchdb-connector:request-update` | `{ id, data, expected_version, idempotencyKey? }` | Triggers a PUT update (expected_version maps to CouchDB's revision). |
91
+ | `ln-couchdb-connector:request-delete` | `{ id, rev, idempotencyKey? }` | Triggers a deletion request. |
92
+ | `ln-couchdb-connector:request-bulk-delete` | `{ ids, idempotencyKey? }` | Triggers a bulk-deletion request. |
93
+
94
+ ### Notifications (Emitted BY the connector)
95
+
96
+ The connector dispatches bubbles-enabled events to notify parent coordinators of response states.
97
+
98
+ | Event | `detail` Payload | Description |
99
+ |-------|------------------|-------------|
100
+ | `ln-couchdb-connector:fetched` | `{ data, since }` | Dispatched upon successful fetch completion. |
101
+ | `ln-couchdb-connector:created` | `{ record, tempId, message, meta }` | Dispatched upon successful server record confirmation. `message` opaque from the response envelope (null if absent). `meta` echoes the request's `meta` unchanged. |
102
+ | `ln-couchdb-connector:updated` | `{ record, id, message, meta }` | Dispatched upon successful server update confirmation. |
103
+ | `ln-couchdb-connector:deleted` | `{ response, id, message, meta }` | Dispatched upon successful server deletion. |
104
+ | `ln-couchdb-connector:bulk-deleted`| `{ response, ids, message, meta }` | Dispatched upon successful server bulk deletion. |
105
+ | `ln-couchdb-connector:error` | `{ action, error, status, meta, ... }` | Dispatched on failures. Update errors additionally include `data`/`conflictData` (same value) if status is 409. `meta` echoes the request's `meta` unchanged — required for coordinator-side queue ack/nack and id-swap reconciliation. |
106
+ | `ln-couchdb-connector:config-changed` | `{ url, db, auth, headers }` | Dispatched when configuration attributes are mutated. |
107
+
108
+ ---
109
+
110
+ ## Mutation Response Envelope
111
+
112
+ `create`/`update`/`delete`/`bulkDelete` presence-check the parsed JSON body for an optional `{ message, content }` envelope, mirroring `ln-api-connector`'s contract:
113
+
114
+ - `content` present → used as the CouchDB response body (`{id, rev}` etc.) in place of the bare body.
115
+ - `message` present → passed opaquely on the `:created`/`:updated`/`:deleted`/`:bulk-deleted` event detail; a coordinator surfaces it as a success toast. Absent → `message: null`, no toast.
116
+ - Raw CouchDB never sends this envelope — `content` falls back to the bare body, `message` is always `null`, so a direct CouchDB backend never produces a toast. A proxy/gateway placed in front of CouchDB (e.g. to add cross-cutting audit messages) can opt in by wrapping its response in `{ message, content }`.
117
+ - `fetchDelta` (sync) is NOT part of this envelope — sync keeps its own `{ data, deleted, synced_at }` collection-delta shape.
118
+ - The JS API methods (`create`, `update`, `delete`, `bulkDelete`) resolve to the SAME shape as before this change (record / record / response / response) — the envelope unwrap is internal; direct JS callers are unaffected. `message` is only surfaced via the DOM-event path (`:created`/`:updated`/`:deleted`/`:bulk-deleted` `detail.message`).
119
+
120
+ ---
121
+
122
+ ## 3-Tier Integration Architecture (Coordinator Example)
123
+
124
+ Here is how a parent Coordinator links a Cache Store and the CouchDB Connector using standard DOM events:
125
+
126
+ ```js
127
+ (function () {
128
+ const parent = document.querySelector('[data-ln-data-coordinator="tasks"]');
129
+ // ln-data-coordinator wires store <-> connector automatically once both
130
+ // are present in its subtree — no hand-written glue is needed for the
131
+ // write pipeline. This snippet only illustrates what the coordinator
132
+ // does internally; in a real page, just declare the markup (see
133
+ // "Declarative DOM Setup" above) and let ln-data-coordinator do the rest.
134
+
135
+ // Server confirmation reconciles via an ORDINARY store update (id-swap),
136
+ // not a special confirm/revert method:
137
+ // connectorEl.addEventListener('ln-couchdb-connector:created', function (e) {
138
+ // const meta = e.detail.meta || {};
139
+ // storeEl.dispatchEvent(new CustomEvent('ln-data-store:request-update', {
140
+ // detail: { id: meta.tempId, data: e.detail.record }
141
+ // }));
142
+ // });
143
+ //
144
+ // See js/ln-data-coordinator/README.md for the full fan-out + error
145
+ // reconciliation policy (auth/transient/deterministic) that ships with
146
+ // the coordinator — this connector never needs to be wired by hand.
147
+ })();
148
+ ```
149
+
150
+ ---
151
+
152
+ ## 🔧 Internals
153
+
154
+ Source: `js/ln-couchdb-connector/*`. Bridges CouchDB's revision-based (`_rev`) document protocol and `_changes`/`_bulk_docs` endpoints to the same coordinator-facing event contract as `ln-api-connector`.
155
+
156
+ ### Sync flow
157
+
158
+ `request-sync {since}` → `GET /{db}/_changes?include_docs=true&since=<since>` → maps `rows.doc` to `{..., id: _id}` and `rows.deleted` to `deletedIds` → `:fetched { data, deleted, synced_at }`. The coordinator hands this straight to `store.applySync`.
159
+
160
+ ### Revision resolution (zero-friction `_rev`)
161
+
162
+ Every update/delete payload is inspected for `_rev`/`rev`. If present, `PUT`/`DELETE` fires immediately with `If-Match`. If absent, the connector first `GET`s the current document to read `_rev`, then issues the mutation — callers never need to track revisions themselves. A `409 Conflict` fires `:error` with `status: 409` and `data`/`conflictData` set to CouchDB's raw `{error, reason}` body. Unlike a REST backend, CouchDB's 409 carries no document snapshot, so the coordinator's server-wins reconciliation is a no-op on this path — only the error toast fires; the optimistic local write stays in place until the next sync reconciles it.
163
+
164
+ ### ID translation
165
+
166
+ Ingress: every fetched/created/updated document gets `id = _id` before reaching the coordinator. Egress: `create`/`update` map `id` back to `_id` before the request body is sent. The mapping is entirely internal — JS API callers and DOM-event consumers never see `_id`.
167
+
168
+ ### Bulk delete
169
+
170
+ 1. `POST /{db}/_all_docs` with `{"keys": [...]}` to resolve current `_rev` per id.
171
+ 2. Filter out any ids that errored or are missing.
172
+ 3. Build `{_id, _rev, _deleted: true}` per surviving doc.
173
+ 4. `POST /{db}/_bulk_docs` with the array.
174
+
175
+ ### Envelope unwrap
176
+
177
+ Same `{message, content}` presence-check as `ln-api-connector` (see its README) — `content` replaces the bare CouchDB body when present, `message` rides opaquely on the `:created`/`:updated`/`:deleted`/`:bulk-deleted` event detail. Raw CouchDB never sends this envelope, so a direct CouchDB backend never toasts; only a proxy/gateway that wraps responses can opt in.
@@ -1 +1 @@
1
- (function(){"use strict";function h(c,d,l){c.dispatchEvent(new CustomEvent(d,{bubbles:!0,detail:l||{}}))}function C(c,d){if(!document.body){document.addEventListener("DOMContentLoaded",function(){C(c,d)}),console.warn("["+d+'] Script loaded before <body> — add "defer" to your <script> tag');return}c()}function _(c,d,l,f){if(c.nodeType!==1)return;const v=d.indexOf("[")!==-1||d.indexOf(".")!==-1||d.indexOf("#")!==-1?d:"["+d+"]",e=Array.from(c.querySelectorAll(v));c.matches&&c.matches(v)&&e.push(c);for(const o of e)o[l]||(o[l]=new f(o))}function x(c,d,l,f,g={}){const v=g.extraAttributes||[],e=g.onAttributeChange||null,o=g.onInit||null;function t(r){const n=r||document.body;_(n,c,d,l),o&&o(n)}return C(function(){const r=new MutationObserver(function(i){for(let s=0;s<i.length;s++){const a=i[s];if(a.type==="childList"){for(let m=0;m<a.addedNodes.length;m++){const u=a.addedNodes[m];u.nodeType===1&&(_(u,c,d,l),o&&o(u))}for(let m=0;m<a.removedNodes.length;m++){const u=a.removedNodes[m];if(u.nodeType===1){const w=c.indexOf("[")!==-1||c.indexOf(".")!==-1||c.indexOf("#")!==-1?c:"["+c+"]",E=Array.from(u.querySelectorAll(w));u.matches&&u.matches(w)&&E.push(u);for(let T=0;T<E.length;T++){const j=E[T];if(!document.contains(j)){const O=j[d];O&&typeof O.destroy=="function"&&O.destroy()}}}}}else a.type==="attributes"&&(e&&a.target[d]?e(a.target,a.attributeName):(_(a.target,c,d,l),o&&o(a.target)))}});let n=[];if(c.indexOf("[")!==-1){const i=/\[([\w-]+)/g;let s;for(;(s=i.exec(c))!==null;)n.push(s[1])}else n.push(c);r.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:n.concat(v)})},f),window[d]=t,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){t(document.body)}):t(document.body),t}function b(...c){return c.filter(d=>d!=null&&d!=="").map((d,l)=>l===0?d.replace(/\/+$/,""):d.replace(/^\/+/,"").replace(/\/+$/,"")).filter(Boolean).join("/")}function p(c,d){return Object.assign({"Content-Type":"application/json",Accept:"application/json"},c,d?{Authorization:d}:null)}function S(c,d="ln-core"){try{return c?JSON.parse(c):{}}catch(l){return console.error(`[${d}] Invalid headers JSON:`,l),{}}}const k={};function L(c,d){k[c]=d}function A(c){return k[c]||{ingress:d=>d,egress:d=>d}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=L,window.lnCore.getDataMapper=A),(function(){const c="data-ln-couchdb-connector",d="lnCouchDbConnector",l="lnConnector";if(window[d]!==void 0)return;function f(e){return this.dom=e,e[d]=this,e[l]=this,this.refreshConfig(),this._handlers=null,g(this),this}f.prototype.refreshConfig=function(){const e=this.dom;this.url=e.getAttribute("data-ln-couchdb-url")||"",this.db=e.getAttribute("data-ln-couchdb-db")||"",this.auth=e.getAttribute("data-ln-couchdb-auth")||"",this.credentials="same-origin";const o=e.getAttribute("data-ln-couchdb-headers")||"";this.headers=S(o,"ln-couchdb-connector"),this.auth&&console.warn("[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-auth attribute. Storing basic authentication credentials in HTML DOM attributes is highly discouraged and vulnerable to XSS credential extraction. Please use HttpOnly session cookies or a Backend Proxy Gateway instead."),o.toLowerCase().includes("authorization")&&console.warn("[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-headers attribute. Please use HttpOnly session cookies or a Backend Proxy Gateway instead."),h(e,"ln-couchdb-connector:config-changed",{url:this.url,db:this.db,auth:this.auth?"[REDACTED]":"",headers:this.headers})},f.prototype.fetchDelta=function(e){const o=this,t=["include_docs=true","feed=normal"];e&&t.push("since="+encodeURIComponent(e));const r=b(o.url,o.db,"_changes")+"?"+t.join("&");return window.fetch(r,{method:"GET",headers:p(o.headers,o.auth),credentials:o.credentials}).then(n=>{if(!n.ok)throw new Error("HTTP "+n.status+": "+n.statusText);return n.json()}).then(n=>{const i=n.results||[];return{data:i.filter(s=>!s.deleted&&s.doc).map(s=>Object.assign({},s.doc,{id:s.doc._id})),deleted:i.filter(s=>s.deleted).map(s=>s.id),synced_at:n.last_seq||e||""}})},f.prototype.create=function(e){const o=this,t=Object.assign({_id:e.id},e);return t._id||delete t._id,window.fetch(b(o.url,o.db),{method:"POST",headers:p(o.headers,o.auth),credentials:o.credentials,body:JSON.stringify(t)}).then(r=>{if(!r.ok)throw new Error("HTTP "+r.status+": "+r.statusText);return r.json()}).then(r=>Object.assign({},t,{id:r.id,_id:r.id,_rev:r.rev}))},f.prototype.update=function(e,o){const t=this,r=Object.assign({id:String(e),_id:String(e)},o),n=r._rev||r.rev;return(n?Promise.resolve(n):window.fetch(b(t.url,t.db,null,e),{method:"GET",headers:p(t.headers,t.auth),credentials:t.credentials}).then(s=>{if(!s.ok)throw new Error("Could not retrieve document for revision mapping");return s.json().then(a=>a._rev)})).then(s=>{const a=Object.assign({},r,{_rev:s});delete a.rev;const m=Object.assign(p(t.headers,t.auth),{"If-Match":s});return window.fetch(b(t.url,t.db,null,e),{method:"PUT",headers:m,credentials:t.credentials,body:JSON.stringify(a)}).then(u=>{if(u.ok)return u.json().then(y=>Object.assign({},a,{_rev:y.rev}));if(u.status===409)return u.json().then(y=>{const w=new Error("Conflict");throw w.status=409,w.data=y,w});throw new Error("HTTP "+u.status+": "+u.statusText)})})},f.prototype.delete=function(e,o){const t=this;return(o?Promise.resolve(o):window.fetch(b(t.url,t.db,null,e),{method:"GET",headers:p(t.headers,t.auth),credentials:t.credentials}).then(n=>{if(!n.ok)throw new Error("Could not retrieve document for revision delete");return n.json().then(i=>i._rev)})).then(n=>{const i=b(t.url,t.db,null,e)+"?rev="+encodeURIComponent(n);return window.fetch(i,{method:"DELETE",headers:p(t.headers,t.auth),credentials:t.credentials}).then(s=>{if(!s.ok)throw new Error("HTTP "+s.status+": "+s.statusText);return s.json()})})},f.prototype.bulkDelete=function(e){const o=this;return!e||e.length===0?Promise.resolve({ok:!0,deletedCount:0}):window.fetch(b(o.url,o.db,"_all_docs"),{method:"POST",headers:p(o.headers,o.auth),credentials:o.credentials,body:JSON.stringify({keys:e})}).then(t=>{if(!t.ok)throw new Error("HTTP "+t.status+": "+t.statusText);return t.json()}).then(t=>{const n=(t.rows||[]).filter(i=>!i.error&&i.value&&i.value.rev).map(i=>({_id:i.id,_rev:i.value.rev,_deleted:!0}));return n.length===0?{ok:!0,deletedCount:0}:window.fetch(b(o.url,o.db,"_bulk_docs"),{method:"POST",headers:p(o.headers,o.auth),credentials:o.credentials,body:JSON.stringify({docs:n})}).then(i=>{if(!i.ok)throw new Error("HTTP "+i.status+": "+i.statusText);return i.json()}).then(i=>({ok:!0,results:i,deletedCount:n.length}))})};function g(e){e._handlers={sync:function(t){const r=t.detail||{};e.fetchDelta(r.since).then(function(n){h(e.dom,"ln-couchdb-connector:fetched",{data:n,since:r.since})}).catch(function(n){h(e.dom,"ln-couchdb-connector:error",{action:"sync",error:n.message,status:n.status||0,since:r.since})})},create:function(t){const r=t.detail||{};e.create(r.data).then(function(n){h(e.dom,"ln-couchdb-connector:created",{record:n,tempId:r.tempId})}).catch(function(n){h(e.dom,"ln-couchdb-connector:error",{action:"create",error:n.message,status:n.status||0,tempId:r.tempId})})},update:function(t){const r=t.detail||{},n=Object.assign({},r.data);r.expected_version!==void 0&&(n._rev=r.expected_version),e.update(r.id,n).then(function(i){h(e.dom,"ln-couchdb-connector:updated",{record:i,id:r.id})}).catch(function(i){h(e.dom,"ln-couchdb-connector:error",{action:"update",error:i.message,status:i.status||0,id:r.id,conflictData:i.status===409?i.data:null})})},delete:function(t){const r=t.detail||{};e.delete(r.id,r.rev).then(function(n){h(e.dom,"ln-couchdb-connector:deleted",{response:n,id:r.id})}).catch(function(n){h(e.dom,"ln-couchdb-connector:error",{action:"delete",error:n.message,status:n.status||0,id:r.id})})},bulkDelete:function(t){const r=t.detail||{};e.bulkDelete(r.ids).then(function(n){h(e.dom,"ln-couchdb-connector:bulk-deleted",{response:n,ids:r.ids})}).catch(function(n){h(e.dom,"ln-couchdb-connector:error",{action:"bulk-delete",error:n.message,status:n.status||0,ids:r.ids})})}},["ln-couchdb-connector","ln-api-connector","ln-rest-connector"].forEach(function(t){e.dom.addEventListener(t+":request-sync",e._handlers.sync),e.dom.addEventListener(t+":request-fetch",e._handlers.sync),e.dom.addEventListener(t+":request-create",e._handlers.create),e.dom.addEventListener(t+":request-update",e._handlers.update),e.dom.addEventListener(t+":request-delete",e._handlers.delete),e.dom.addEventListener(t+":request-bulk-delete",e._handlers.bulkDelete)})}f.prototype.destroy=function(){if(!this.dom[d])return;const e=this;e._handlers&&(["ln-couchdb-connector","ln-api-connector","ln-rest-connector"].forEach(function(t){e.dom.removeEventListener(t+":request-sync",e._handlers.sync),e.dom.removeEventListener(t+":request-fetch",e._handlers.sync),e.dom.removeEventListener(t+":request-create",e._handlers.create),e.dom.removeEventListener(t+":request-update",e._handlers.update),e.dom.removeEventListener(t+":request-delete",e._handlers.delete),e.dom.removeEventListener(t+":request-bulk-delete",e._handlers.bulkDelete)}),e._handlers=null),h(this.dom,"ln-couchdb-connector:destroyed",{target:this.dom}),delete this.dom[d],delete this.dom[l]};function v(e){const o=e[d];o&&o.refreshConfig()}x(c,d,f,"ln-couchdb-connector",{extraAttributes:["data-ln-couchdb-url","data-ln-couchdb-db","data-ln-couchdb-auth","data-ln-couchdb-headers"],onAttributeChange:v})})()})();
1
+ (function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...n){typeof n[0]=="string"&&(n[0].startsWith("[ln-")||n[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,n)}}const L={};function M(t,n){L[t]||(L[t]=document.querySelector('[data-ln-template="'+t+'"]'));const c=L[t];return c?c.content.cloneNode(!0):(console.warn("["+(n||"ln-core")+'] Template "'+t+'" not found'),null)}function v(t,n,c){t.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:c||{}}))}function j(t,n){if(!t||!n)return t;const c=t.querySelectorAll("[data-ln-field]");for(let d=0;d<c.length;d++){const a=c[d],l=a.getAttribute("data-ln-field");n[l]!=null&&(a.textContent=n[l])}const g=t.querySelectorAll("[data-ln-attr]");for(let d=0;d<g.length;d++){const a=g[d],l=a.getAttribute("data-ln-attr").split(",");for(let p=0;p<l.length;p++){const b=l[p].trim().split(":");if(b.length!==2)continue;const y=b[0].trim(),e=b[1].trim();n[e]!=null&&a.setAttribute(y,n[e])}}const m=t.querySelectorAll("[data-ln-show]");for(let d=0;d<m.length;d++){const a=m[d],l=a.getAttribute("data-ln-show");l in n&&a.classList.toggle("hidden",!n[l])}const h=t.querySelectorAll("[data-ln-class]");for(let d=0;d<h.length;d++){const a=h[d],l=a.getAttribute("data-ln-class").split(",");for(let p=0;p<l.length;p++){const b=l[p].trim().split(":");if(b.length!==2)continue;const y=b[0].trim(),e=b[1].trim();e in n&&a.classList.toggle(y,!!n[e])}}return t}function R(t,n){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:n??null,bubbles:!0}));const c=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let g=0;g<c.length;g++)c[g].dispatchEvent(new CustomEvent("ln-fill",{detail:n??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)j(t.target,t.detail);else{const n=t.target.querySelectorAll("[data-ln-field]");for(let c=0;c<n.length;c++)n[c].textContent=""}})));function D(t,n){if(!t||!n)return t;const c=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;c.nextNode();){const h=c.currentNode;h.textContent.indexOf("{{")!==-1&&(h.textContent=h.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(d,a){return n[a]!==void 0?n[a]:""}))}const g=function(h,d){return n[d]!==void 0?n[d]:""},m=Array.from(t.querySelectorAll("*"));t.nodeType===1&&m.push(t);for(let h=0;h<m.length;h++){const d=m[h],a=d.attributes;for(let l=0;l<a.length;l++){const p=a[l];p.value.indexOf("{{")!==-1&&d.setAttribute(p.name,p.value.replace(/\{\{\s*(\w+)\s*\}\}/g,g))}}return t}function B(t,n,c,g,m,h){const d={};for(let l=0;l<t.children.length;l++){const p=t.children[l],b=p.getAttribute("data-ln-key");b&&(d[b]=p)}const a=document.createDocumentFragment();for(let l=0;l<n.length;l++){const p=n[l],b=String(g(p));let y=d[b];if(y)m(y,p,l);else{const e=M(c,h);if(!e||(D(e,p),y=e.firstElementChild,!y))continue;y.setAttribute("data-ln-key",b),m(y,p,l)}a.appendChild(y)}t.textContent="",t.appendChild(a)}function q(t,n){if(!document.body){document.addEventListener("DOMContentLoaded",function(){q(t,n)}),console.warn("["+n+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function O(t,n,c,g){if(t.nodeType!==1)return;const h=n.indexOf("[")!==-1||n.indexOf(".")!==-1||n.indexOf("#")!==-1?n:"["+n+"]",d=Array.from(t.querySelectorAll(h));t.matches&&t.matches(h)&&d.push(t);for(const a of d)a[c]||(a[c]=new g(a))}function U(t,n,c,g,m={}){const h=m.extraAttributes||[],d=m.onAttributeChange||null,a=m.onInit||null;function l(p){const b=p||document.body;O(b,t,n,c),a&&a(b)}return q(function(){const p=new MutationObserver(function(y){for(let e=0;e<y.length;e++){const i=y[e];if(i.type==="childList"){for(let r=0;r<i.addedNodes.length;r++){const o=i.addedNodes[r];o.nodeType===1&&(O(o,t,n,c),a&&a(o))}for(let r=0;r<i.removedNodes.length;r++){const o=i.removedNodes[r];if(o.nodeType===1){const f=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",u=Array.from(o.querySelectorAll(f));o.matches&&o.matches(f)&&u.push(o);for(let w=0;w<u.length;w++){const C=u[w];if(!document.contains(C)){const A=C[n];A&&typeof A.destroy=="function"&&A.destroy()}}}}}else i.type==="attributes"&&(d&&i.target[n]?d(i.target,i.attributeName):(O(i.target,t,n,c),a&&a(i.target)))}});let b=[];if(t.indexOf("[")!==-1){const y=/\[([\w-]+)/g;let e;for(;(e=y.exec(t))!==null;)b.push(e[1])}else b.push(t);p.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:b.concat(h)})},g),window[n]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}function _(...t){return t.filter(n=>n!=null&&n!=="").map((n,c)=>c===0?n.replace(/\/+$/,""):n.replace(/^\/+/,"").replace(/\/+$/,"")).filter(Boolean).join("/")}function k(t,n){return Object.assign({"Content-Type":"application/json",Accept:"application/json"},t,n?{Authorization:n}:null)}function W(t,n="ln-core"){try{return t?JSON.parse(t):{}}catch(c){return console.error(`[${n}] Invalid headers JSON:`,c),{}}}const P={};function K(t,n){P[t]=n}function G(t){return P[t]||{ingress:n=>n,egress:n=>n}}const I={};function N(t,n){if(!t||typeof n!="object")return;const c=t.toLowerCase().split("-")[0];I[c]=n}function F(t){if(!t)return null;const n=t.toLowerCase().split("-")[0];return I[n]||null}N("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=K,window.lnCore.getDataMapper=G,window.lnCore.registerLocaleFallback=N,window.lnCore.getLocaleFallback=F,window.lnCore.fillTemplate=D,window.lnCore.fill=j,window.lnCore.lnFill=R,window.lnCore.renderList=B);function H(t){return(t||"").replace(/^#/,"")}function S(t){const n=t===void 0?location.hash:t,c={},g=H(n);if(!g)return c;const m=g.split("&");for(let h=0;h<m.length;h++){const d=m[h];if(!d)continue;const a=d.indexOf(":"),l=a>-1?d.slice(0,a):d,p=a>-1?d.slice(a+1):"";if(l)try{c[l]=decodeURIComponent(p)}catch{c[l]=p}}return c}function J(t){if(!t)return null;const n=S();return t in n?n[t]:null}function z(t,n){if(!t)return;const c=S();n==null?delete c[t]:c[t]=String(n);const m=Object.keys(c).map(function(h){const d=c[h];return d===""?h:h+":"+encodeURIComponent(d)}).join("&");H(location.hash)!==m&&(location.hash=m)}function $(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=S,window.lnCore.hashGet=J,window.lnCore.hashSet=z,window.lnCore.hashLinkClick=$),(function(){const t="data-ln-couchdb-connector",n="lnCouchDbConnector",c="lnConnector";if(window[n]!==void 0)return;function g(e){const i=e&&e.content!==void 0?e.content:e,r=e&&e.message?e.message:null;return{content:i,message:r}}function m(e){return this.dom=e,e[n]=this,e[c]=this,this.refreshConfig(),this._handlers=null,b(this),this}m.prototype.refreshConfig=function(){const e=this.dom;this.url=e.getAttribute("data-ln-couchdb-url")||"",this.db=e.getAttribute("data-ln-couchdb-db")||"",this.auth=e.getAttribute("data-ln-couchdb-auth")||"",this.credentials="same-origin";const i=e.getAttribute("data-ln-couchdb-headers")||"";this.headers=W(i,"ln-couchdb-connector"),this.auth&&console.warn("[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-auth attribute. Storing basic authentication credentials in HTML DOM attributes is highly discouraged and vulnerable to XSS credential extraction. Please use HttpOnly session cookies or a Backend Proxy Gateway instead."),i.toLowerCase().includes("authorization")&&console.warn("[ln-couchdb-connector] Security Warning: Sensitive authorization credentials detected in data-ln-couchdb-headers attribute. Please use HttpOnly session cookies or a Backend Proxy Gateway instead."),v(e,"ln-couchdb-connector:config-changed",{url:this.url,db:this.db,auth:this.auth?"[REDACTED]":"",headers:this.headers})};function h(e,i,r){const o=Object.assign({},k(e.headers,e.auth),r||{});return i&&(o["Idempotency-Key"]=i),o}m.prototype.fetchDelta=function(e){const i=this,r=["include_docs=true","feed=normal"];e&&r.push("since="+encodeURIComponent(e));const o=_(i.url,i.db,"_changes")+"?"+r.join("&");return window.fetch(o,{method:"GET",headers:k(i.headers,i.auth),credentials:i.credentials}).then(s=>{if(!s.ok)throw new Error("HTTP "+s.status+": "+s.statusText);return s.json()}).then(s=>{const f=s.results||[];return{data:f.filter(u=>!u.deleted&&u.doc).map(u=>Object.assign({},u.doc,{id:u.doc._id})),deleted:f.filter(u=>u.deleted).map(u=>u.id),synced_at:s.last_seq||e||""}})};function d(e,i,r){const o=Object.assign({_id:i.id},i);return o._id||delete o._id,window.fetch(_(e.url,e.db),{method:"POST",headers:h(e,r),credentials:e.credentials,body:JSON.stringify(o)}).then(s=>{if(!s.ok)throw new Error("HTTP "+s.status+": "+s.statusText);return s.json()}).then(s=>{const f=g(s),u=f.content;return{record:Object.assign({},o,{id:u.id,_id:u.id,_rev:u.rev}),message:f.message}})}m.prototype.create=function(e,i){return d(this,e,i).then(r=>r.record)};function a(e,i,r,o){const s=Object.assign({id:String(i),_id:String(i)},r),f=s._rev||s.rev;return(f?Promise.resolve(f):window.fetch(_(e.url,e.db,null,i),{method:"GET",headers:k(e.headers,e.auth),credentials:e.credentials}).then(w=>{if(!w.ok)throw new Error("Could not retrieve document for revision mapping");return w.json().then(C=>C._rev)})).then(w=>{const C=Object.assign({},s,{_rev:w});delete C.rev;const A=h(e,o,{"If-Match":w});return window.fetch(_(e.url,e.db,null,i),{method:"PUT",headers:A,credentials:e.credentials,body:JSON.stringify(C)}).then(E=>{if(E.ok)return E.json().then(x=>{const T=g(x);return{record:Object.assign({},C,{_rev:T.content.rev}),message:T.message}});if(E.status===409)return E.json().then(x=>{const T=new Error("Conflict");throw T.status=409,T.data=x,T});throw new Error("HTTP "+E.status+": "+E.statusText)})})}m.prototype.update=function(e,i,r){return a(this,e,i,r).then(o=>o.record)};function l(e,i,r,o){return(r?Promise.resolve(r):window.fetch(_(e.url,e.db,null,i),{method:"GET",headers:k(e.headers,e.auth),credentials:e.credentials}).then(f=>{if(!f.ok)throw new Error("Could not retrieve document for revision delete");return f.json().then(u=>u._rev)})).then(f=>{const u=_(e.url,e.db,null,i)+"?rev="+encodeURIComponent(f);return window.fetch(u,{method:"DELETE",headers:h(e,o),credentials:e.credentials}).then(w=>{if(!w.ok)throw new Error("HTTP "+w.status+": "+w.statusText);return w.json()}).then(w=>{const C=g(w);return{response:C.content,message:C.message}})})}m.prototype.delete=function(e,i,r){return l(this,e,i,r).then(o=>o.response)};function p(e,i,r){return!i||i.length===0?Promise.resolve({response:{ok:!0,deletedCount:0},message:null}):window.fetch(_(e.url,e.db,"_all_docs"),{method:"POST",headers:k(e.headers,e.auth),credentials:e.credentials,body:JSON.stringify({keys:i})}).then(o=>{if(!o.ok)throw new Error("HTTP "+o.status+": "+o.statusText);return o.json()}).then(o=>{const f=(o.rows||[]).filter(u=>!u.error&&u.value&&u.value.rev).map(u=>({_id:u.id,_rev:u.value.rev,_deleted:!0}));return f.length===0?{response:{ok:!0,deletedCount:0},message:null}:window.fetch(_(e.url,e.db,"_bulk_docs"),{method:"POST",headers:h(e,r),credentials:e.credentials,body:JSON.stringify({docs:f})}).then(u=>{if(!u.ok)throw new Error("HTTP "+u.status+": "+u.statusText);return u.json()}).then(u=>{const w=g(u);return{response:{ok:!0,results:w.content,deletedCount:f.length},message:w.message}})})}m.prototype.bulkDelete=function(e,i){return p(this,e,i).then(r=>r.response)};function b(e){e._handlers={sync:function(r){const o=r.detail||{};e.fetchDelta(o.since).then(function(s){v(e.dom,"ln-couchdb-connector:fetched",{data:s,since:o.since,meta:o.meta||null})}).catch(function(s){v(e.dom,"ln-couchdb-connector:error",{action:"sync",error:s.message,status:s.status||0,since:o.since,meta:o.meta||null})})},create:function(r){const o=r.detail||{};d(e,o.data,o.idempotencyKey).then(function(s){v(e.dom,"ln-couchdb-connector:created",{record:s.record,tempId:o.tempId,message:s.message,meta:o.meta||null})}).catch(function(s){v(e.dom,"ln-couchdb-connector:error",{action:"create",error:s.message,status:s.status||0,tempId:o.tempId,meta:o.meta||null})})},update:function(r){const o=r.detail||{},s=Object.assign({},o.data);o.expected_version!==void 0&&(s._rev=o.expected_version),a(e,o.id,s,o.idempotencyKey).then(function(f){v(e.dom,"ln-couchdb-connector:updated",{record:f.record,id:o.id,message:f.message,meta:o.meta||null})}).catch(function(f){v(e.dom,"ln-couchdb-connector:error",{action:"update",error:f.message,status:f.status||0,id:o.id,data:f.status===409?f.data:null,conflictData:f.status===409?f.data:null,meta:o.meta||null})})},delete:function(r){const o=r.detail||{};l(e,o.id,o.rev,o.idempotencyKey).then(function(s){v(e.dom,"ln-couchdb-connector:deleted",{response:s.response,id:o.id,message:s.message,meta:o.meta||null})}).catch(function(s){v(e.dom,"ln-couchdb-connector:error",{action:"delete",error:s.message,status:s.status||0,id:o.id,meta:o.meta||null})})},bulkDelete:function(r){const o=r.detail||{};p(e,o.ids,o.idempotencyKey).then(function(s){v(e.dom,"ln-couchdb-connector:bulk-deleted",{response:s.response,ids:o.ids,message:s.message,meta:o.meta||null})}).catch(function(s){v(e.dom,"ln-couchdb-connector:error",{action:"bulk-delete",error:s.message,status:s.status||0,ids:o.ids,meta:o.meta||null})})}},["ln-couchdb-connector","ln-api-connector","ln-rest-connector"].forEach(function(r){e.dom.addEventListener(r+":request-sync",e._handlers.sync),e.dom.addEventListener(r+":request-fetch",e._handlers.sync),e.dom.addEventListener(r+":request-create",e._handlers.create),e.dom.addEventListener(r+":request-update",e._handlers.update),e.dom.addEventListener(r+":request-delete",e._handlers.delete),e.dom.addEventListener(r+":request-bulk-delete",e._handlers.bulkDelete)})}m.prototype.destroy=function(){if(!this.dom[n])return;const e=this;e._handlers&&(["ln-couchdb-connector","ln-api-connector","ln-rest-connector"].forEach(function(r){e.dom.removeEventListener(r+":request-sync",e._handlers.sync),e.dom.removeEventListener(r+":request-fetch",e._handlers.sync),e.dom.removeEventListener(r+":request-create",e._handlers.create),e.dom.removeEventListener(r+":request-update",e._handlers.update),e.dom.removeEventListener(r+":request-delete",e._handlers.delete),e.dom.removeEventListener(r+":request-bulk-delete",e._handlers.bulkDelete)}),e._handlers=null),v(this.dom,"ln-couchdb-connector:destroyed",{target:this.dom}),delete this.dom[n],delete this.dom[c]};function y(e){const i=e[n];i&&i.refreshConfig()}U(t,n,m,"ln-couchdb-connector",{extraAttributes:["data-ln-couchdb-url","data-ln-couchdb-db","data-ln-couchdb-auth","data-ln-couchdb-headers"],onAttributeChange:y})})()})();