@livenetworks/ashlar 1.5.1 → 1.6.1

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 +165 -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 +82 -42
  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
@@ -1,249 +1,1042 @@
1
- import { registerComponent, dispatch } from '../../ln-core';
2
-
3
- (function () {
4
- const DOM_SELECTOR = 'data-ln-data-coordinator';
5
- const DOM_ATTRIBUTE = 'lnDataCoordinator';
6
- const DOM_ALIAS = 'lnCoordinator';
7
-
8
- if (window[DOM_ATTRIBUTE] !== undefined) return;
9
-
10
- // ─── Component Constructor ─────────────────────────────
11
-
12
- function _component(dom) {
13
- this.dom = dom;
14
- this._name = dom.getAttribute(DOM_SELECTOR);
15
- dom[DOM_ATTRIBUTE] = this;
16
- dom[DOM_ALIAS] = this;
17
-
18
- this.mapper = null;
19
- this._handlers = null;
20
-
21
- this.refreshMapper();
22
- _bindEvents(this);
23
-
24
- return this;
25
- }
26
-
27
- // ─── Resolve and Refresh Mapper ──────────────────────────
28
-
29
- _component.prototype.refreshMapper = function () {
30
- this.mapper = null;
31
-
32
- // 1. Check for deprecated/insecure inline script mapper
33
- const inlineScript = this.dom.querySelector('script[data-ln-mapper]');
34
- if (inlineScript) {
35
- console.error('[ln-data-coordinator] Security Error: Inline script mappers using <script data-ln-mapper> are deprecated and disabled due to XSS vulnerability risks (unsafe-eval). Please register your mappers securely via window.lnCore.registerDataMapper() instead.');
36
- }
37
-
38
- // 2. Resolve to registered external mapper
39
- const mapperName = this.dom.getAttribute('data-ln-data-mapper') || this.dom.getAttribute('data-ln-data-coordinator');
40
- if (mapperName && window.lnCore && typeof window.lnCore.getDataMapper === 'function') {
41
- this.mapper = window.lnCore.getDataMapper(mapperName);
42
- }
43
-
44
- // 3. Ultimate safe fallback: no-op mapper
45
- if (!this.mapper) {
46
- this.mapper = {};
47
- }
48
-
49
- // Ensure ingress and egress are safe callable functions
50
- if (typeof this.mapper.ingress !== 'function') {
51
- this.mapper.ingress = function (r) { return r; };
52
- }
53
- if (typeof this.mapper.egress !== 'function') {
54
- this.mapper.egress = function (r) { return r; };
55
- }
56
- };
57
-
58
- // ─── Dynamic Child Discovery ──────────────────────────────
59
-
60
- _component.prototype.findChildren = function () {
61
- const storeEl = this.dom.querySelector('[data-ln-data-store]');
62
- const connectorEl = this.dom.querySelector('[data-ln-api-connector], [data-ln-couchdb-connector], [data-ln-websocket-connector], [data-ln-rest-connector]');
63
-
64
- return {
65
- storeEl: storeEl,
66
- connectorEl: connectorEl,
67
- store: storeEl ? (storeEl.lnDataStore || storeEl.lnStore) : null,
68
- connector: connectorEl ? (connectorEl.lnConnector || connectorEl.lnApiConnector || connectorEl.lnCouchDbConnector) : null
69
- };
70
- };
71
-
72
- // ─── Event Binding ────────────────────────────────────────
73
-
74
- function _bindEvents(self) {
75
- self._handlers = {
76
- sync: function (e) {
77
- self.refreshMapper();
78
- const children = self.findChildren();
79
- if (!children.store || !children.connector) {
80
- console.warn('[ln-data-coordinator] Cannot sync: store or connector not found in subtree');
81
- return;
82
- }
83
-
84
- const since = e.detail.since;
85
-
86
- children.connector.fetchDelta(since)
87
- .then(function (rawResponse) {
88
- let rawRecords = [];
89
- let deletedIds = [];
90
- let syncedAt = null;
91
-
92
- if (rawResponse && Array.isArray(rawResponse)) {
93
- rawRecords = rawResponse;
94
- syncedAt = Math.floor(Date.now() / 1000);
95
- } else if (rawResponse) {
96
- rawRecords = Array.isArray(rawResponse.data) ? rawResponse.data : [];
97
- deletedIds = Array.isArray(rawResponse.deleted) ? rawResponse.deleted : [];
98
- syncedAt = rawResponse.synced_at !== undefined ? rawResponse.synced_at : (rawResponse.since !== undefined ? rawResponse.since : null);
99
- }
100
-
101
- const normalizedData = rawRecords.map(r => self.mapper.ingress(r));
102
- children.store.applySync(normalizedData, deletedIds, syncedAt);
103
- })
104
- .catch(function (err) {
105
- console.error('[ln-data-coordinator] Sync failed:', err);
106
- });
107
- },
108
-
109
- create: function (e) {
110
- self.refreshMapper();
111
- const children = self.findChildren();
112
- if (!children.store || !children.connector) return;
113
-
114
- const tempId = e.detail.tempId;
115
- const data = e.detail.data || {};
116
-
117
- // Apply egress mapping
118
- const serverPayload = self.mapper.egress(data);
119
-
120
- children.connector.create(serverPayload)
121
- .then(function (serverRawResponse) {
122
- const confirmedLocalRecord = self.mapper.ingress(serverRawResponse);
123
- children.store.confirmMutation(tempId, confirmedLocalRecord, 'create');
124
- })
125
- .catch(function (err) {
126
- console.error('[ln-data-coordinator] Create mutation failed:', err);
127
- children.store.revertMutation(tempId, 'create', err.message || err);
128
- });
129
- },
130
-
131
- update: function (e) {
132
- self.refreshMapper();
133
- const children = self.findChildren();
134
- if (!children.store || !children.connector) return;
135
-
136
- const id = e.detail.id;
137
- const expectedVersion = e.detail.expected_version;
138
-
139
- // Fetch complete merged local record to pass to egress for full PUT mapping
140
- children.store.getById(id)
141
- .then(function (localRecord) {
142
- if (!localRecord) throw new Error('Record not found in cache store: ' + id);
143
-
144
- const cleanRecord = Object.assign({}, localRecord);
145
- delete cleanRecord._pending;
146
-
147
- const serverPayload = self.mapper.egress(cleanRecord);
148
-
149
- return children.connector.update(id, serverPayload, expectedVersion);
150
- })
151
- .then(function (serverRawResponse) {
152
- const confirmedLocalRecord = self.mapper.ingress(serverRawResponse);
153
- children.store.confirmMutation(id, confirmedLocalRecord, 'update');
154
- })
155
- .catch(function (err) {
156
- console.error('[ln-data-coordinator] Update mutation failed:', err);
157
- if (err.status === 409) {
158
- const remoteRecord = err.data && err.data.remote ? self.mapper.ingress(err.data.remote) : null;
159
- const fieldDiffs = err.data ? err.data.field_diffs : null;
160
- children.store.resolveConflict(id, remoteRecord, fieldDiffs);
161
- } else {
162
- children.store.revertMutation(id, 'update', err.message || err);
163
- }
164
- });
165
- },
166
-
167
- delete: function (e) {
168
- self.refreshMapper();
169
- const children = self.findChildren();
170
- if (!children.store || !children.connector) return;
171
-
172
- const id = e.detail.id;
173
-
174
- children.connector.delete(id)
175
- .then(function () {
176
- children.store.confirmMutation(id, null, 'delete');
177
- })
178
- .catch(function (err) {
179
- console.error('[ln-data-coordinator] Delete mutation failed:', err);
180
- children.store.revertMutation(id, 'delete', err.message || err);
181
- });
182
- },
183
-
184
- bulkDelete: function (e) {
185
- self.refreshMapper();
186
- const children = self.findChildren();
187
- if (!children.store || !children.connector) return;
188
-
189
- const ids = e.detail.ids || [];
190
- const bulkKey = ids.join(',');
191
-
192
- children.connector.bulkDelete(ids)
193
- .then(function () {
194
- children.store.confirmMutation(bulkKey, null, 'bulk-delete');
195
- })
196
- .catch(function (err) {
197
- console.error('[ln-data-coordinator] Bulk delete mutation failed:', err);
198
- children.store.revertMutation(bulkKey, 'bulk-delete', err.message || err);
199
- });
200
- }
201
- };
202
-
203
- // Listen to all request-remote events bubbling up from the child store
204
- self.dom.addEventListener('ln-store:request-remote-sync', self._handlers.sync);
205
- self.dom.addEventListener('ln-store:request-remote-create', self._handlers.create);
206
- self.dom.addEventListener('ln-store:request-remote-update', self._handlers.update);
207
- self.dom.addEventListener('ln-store:request-remote-delete', self._handlers.delete);
208
- self.dom.addEventListener('ln-store:request-remote-bulk-delete', self._handlers.bulkDelete);
209
- }
210
-
211
- // ─── Destroy and Cleanup ──────────────────────────────────
212
-
213
- _component.prototype.destroy = function () {
214
- if (!this.dom[DOM_ATTRIBUTE]) return;
215
-
216
- const self = this;
217
- if (self._handlers) {
218
- self.dom.removeEventListener('ln-store:request-remote-sync', self._handlers.sync);
219
- self.dom.removeEventListener('ln-store:request-remote-create', self._handlers.create);
220
- self.dom.removeEventListener('ln-store:request-remote-update', self._handlers.update);
221
- self.dom.removeEventListener('ln-store:request-remote-delete', self._handlers.delete);
222
- self.dom.removeEventListener('ln-store:request-remote-bulk-delete', self._handlers.bulkDelete);
223
- self._handlers = null;
224
- }
225
-
226
- delete this.dom[DOM_ATTRIBUTE];
227
- delete this.dom[DOM_ALIAS];
228
- };
229
-
230
- // ─── Attribute Sync ────────────────────────────────────────
231
-
232
- function _syncAttribute(el, attrName) {
233
- const instance = el[DOM_ATTRIBUTE];
234
- if (!instance) return;
235
-
236
- if (attrName === 'data-ln-data-mapper') {
237
- instance.refreshMapper();
238
- }
239
- }
240
-
241
- // ─── Registration ──────────────────────────────────────
242
-
243
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-data-coordinator', {
244
- extraAttributes: [
245
- 'data-ln-data-mapper'
246
- ],
247
- onAttributeChange: _syncAttribute
248
- });
249
- })();
1
+ import { registerComponent, dispatch, buildDict, serializeForm, resolveFormMethod } from '../../ln-core';
2
+ import { normalizeDataQuery, selectDataSource } from './data-read-policy';
3
+ import { MutationReceipts } from './mutation-receipts';
4
+
5
+ (function () {
6
+ const DOM_SELECTOR = 'data-ln-data-coordinator';
7
+ const DOM_ATTRIBUTE = 'lnDataCoordinator';
8
+ const DOM_ALIAS = 'lnCoordinator';
9
+ const SCOPE_ATTR = 'data-ln-form-scope';
10
+
11
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
12
+
13
+ // ─── Sync Orchestration Singleton ──────────────────────
14
+
15
+ const _coordinators = new Set();
16
+ let _globalSyncInstalled = false;
17
+ let _onlineHandler = null;
18
+ let _offlineHandler = null;
19
+ let _visibilityHandler = null;
20
+
21
+ function _installGlobalSync() {
22
+ if (_globalSyncInstalled) return;
23
+ _globalSyncInstalled = true;
24
+
25
+ _onlineHandler = function () {
26
+ dispatch(document, 'ln-data-store:online', {});
27
+ _coordinators.forEach(function (coord) {
28
+ coord._maybeSync();
29
+ });
30
+ };
31
+
32
+ _offlineHandler = function () {
33
+ dispatch(document, 'ln-data-store:offline', {});
34
+ };
35
+
36
+ _visibilityHandler = function () {
37
+ if (document.visibilityState !== 'visible') return;
38
+ _coordinators.forEach(function (coord) {
39
+ const children = coord.findChildren();
40
+ const store = children.store;
41
+ if (store && children.connector && store.isInitialized && !store.initializationError && !store.isSyncing && !coord._noAutosync && (!store.hasCache || coord._isStale())) {
42
+ store.forceSync();
43
+ }
44
+ });
45
+ };
46
+
47
+ window.addEventListener('online', _onlineHandler);
48
+ window.addEventListener('offline', _offlineHandler);
49
+ document.addEventListener('visibilitychange', _visibilityHandler);
50
+ }
51
+
52
+ function _uninstallGlobalSync() {
53
+ if (!_globalSyncInstalled) return;
54
+ if (_coordinators.size > 0) return;
55
+
56
+ window.removeEventListener('online', _onlineHandler);
57
+ window.removeEventListener('offline', _offlineHandler);
58
+ document.removeEventListener('visibilitychange', _visibilityHandler);
59
+
60
+ _onlineHandler = null;
61
+ _offlineHandler = null;
62
+ _visibilityHandler = null;
63
+ _globalSyncInstalled = false;
64
+ }
65
+
66
+ // ─── Local Helpers ──────────────────────────────────────
67
+
68
+ function _uuid() {
69
+ try { return crypto.randomUUID(); }
70
+ catch (_) {
71
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
72
+ const r = Math.random() * 16 | 0;
73
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
74
+ return v.toString(16);
75
+ });
76
+ }
77
+ }
78
+
79
+ // Connector response namespaces — generalized so writes work whether the
80
+ // paired connector is ln-api-connector or ln-couchdb-connector.
81
+ const CONNECTOR_RESPONSE_NAMESPACES = ['ln-api-connector', 'ln-couchdb-connector'];
82
+
83
+ // ─── Component Constructor ─────────────────────────────
84
+
85
+ function _component(dom) {
86
+ this.dom = dom;
87
+ this._name = dom.getAttribute(DOM_SELECTOR);
88
+ dom[DOM_ATTRIBUTE] = this;
89
+ dom[DOM_ALIAS] = this;
90
+
91
+ this.mapper = null;
92
+ this._handlers = null;
93
+ this._boundQueries = new WeakMap();
94
+ this._boundDelivered = new WeakMap();
95
+ this._mutationReceipts = new MutationReceipts();
96
+ this._dict = buildDict(dom, 'data-ln-data-coordinator-dict'); // flat key→string error-toast map; {} if none
97
+
98
+ this._parseStaleAttributes();
99
+
100
+ this.refreshMapper();
101
+ _bindEvents(this);
102
+
103
+ _coordinators.add(this);
104
+ _installGlobalSync();
105
+
106
+ this._checkInitialSync();
107
+
108
+ return this;
109
+ }
110
+
111
+ // ─── Stale / No-Autosync Attribute Parsing ──────────────
112
+
113
+ _component.prototype._parseStaleAttributes = function () {
114
+ const children = this.findChildren();
115
+ const storeEl = children.storeEl;
116
+
117
+ const staleAttr = this.dom.getAttribute('data-ln-data-coordinator-stale')
118
+ || (storeEl ? storeEl.getAttribute('data-ln-data-store-stale') : null);
119
+ const parsed = parseInt(staleAttr, 10);
120
+ this._staleThreshold = (staleAttr === 'never' || staleAttr === '-1') ? -1 : (isNaN(parsed) ? 300 : parsed);
121
+
122
+ const noAutosyncAttr = this.dom.hasAttribute('data-ln-data-coordinator-no-autosync')
123
+ || (storeEl ? storeEl.hasAttribute('data-ln-data-store-no-autosync') : false);
124
+ this._noAutosync = !!noAutosyncAttr;
125
+ };
126
+
127
+ _component.prototype._isStale = function () {
128
+ if (this._staleThreshold === -1) return false;
129
+ const children = this.findChildren();
130
+ const store = children.store;
131
+ if (!store || !store.lastSyncedAt) return true;
132
+ const ageSeconds = (Date.now() / 1000) - store.lastSyncedAt;
133
+ return ageSeconds > this._staleThreshold;
134
+ };
135
+
136
+ _component.prototype._maybeSync = function () {
137
+ const children = this.findChildren();
138
+ const store = children.store;
139
+ if (!store || store.initializationError || !children.connector || this._noAutosync) return;
140
+ if (!store.isInitialized || store.isSyncing) return;
141
+ if (!store.hasCache || this._isStale()) store.forceSync();
142
+ };
143
+
144
+ // ─── Race Guard: evaluate initial sync directly at children-resolve ────
145
+
146
+ _component.prototype._checkInitialSync = function () {
147
+ const self = this;
148
+ const initial = this.findChildren();
149
+ const store = initial.store;
150
+ if (!store) return;
151
+
152
+ Promise.resolve(store.ready).then(function () {
153
+ const children = self.findChildren();
154
+ const currentStore = children.store;
155
+ if (currentStore && currentStore.initializationError) {
156
+ self._reportReconciliationError('store-initialize', currentStore.initializationError, null);
157
+ return;
158
+ }
159
+ if (!currentStore || !children.connector || self._noAutosync || currentStore.isSyncing) return;
160
+ if (!currentStore.hasCache || self._isStale()) currentStore.forceSync();
161
+ }).catch(function (error) {
162
+ self._reportReconciliationError('store-initialize', error, null);
163
+ });
164
+ };
165
+
166
+ // ─── Resolve and Refresh Mapper ──────────────────────────
167
+
168
+ _component.prototype.refreshMapper = function () {
169
+ this.mapper = null;
170
+
171
+ // 1. Check for deprecated/insecure inline script mapper
172
+ const inlineScript = this.dom.querySelector('script[data-ln-mapper]');
173
+ if (inlineScript) {
174
+ console.error('[ln-data-coordinator] Security Error: Inline script mappers using <script data-ln-mapper> are deprecated and disabled due to XSS vulnerability risks (unsafe-eval). Please register your mappers securely via window.lnCore.registerDataMapper() instead.');
175
+ }
176
+
177
+ // 2. Resolve to registered external mapper
178
+ const mapperName = this.dom.getAttribute('data-ln-data-mapper') || this.dom.getAttribute('data-ln-data-coordinator');
179
+ if (mapperName && window.lnCore && typeof window.lnCore.getDataMapper === 'function') {
180
+ this.mapper = window.lnCore.getDataMapper(mapperName);
181
+ }
182
+
183
+ // 3. Ultimate safe fallback: no-op mapper
184
+ if (!this.mapper) {
185
+ this.mapper = {};
186
+ }
187
+
188
+ // Ensure ingress and egress are safe callable functions
189
+ if (typeof this.mapper.ingress !== 'function') {
190
+ this.mapper.ingress = function (r) { return r; };
191
+ }
192
+ if (typeof this.mapper.egress !== 'function') {
193
+ this.mapper.egress = function (r) { return r; };
194
+ }
195
+ };
196
+
197
+ // ─── Dynamic Child Discovery ──────────────────────────────
198
+
199
+ _component.prototype.findChildren = function () {
200
+ const storeEl = this.dom.querySelector('[data-ln-data-store]');
201
+ const connectorEl = this.dom.querySelector('[data-ln-api-connector], [data-ln-couchdb-connector], [data-ln-websocket-connector], [data-ln-rest-connector]');
202
+ const queueEl = this.dom.querySelector('[data-ln-api-queue]');
203
+
204
+ return {
205
+ storeEl: storeEl,
206
+ connectorEl: connectorEl,
207
+ queueEl: queueEl,
208
+ store: storeEl ? (storeEl.lnDataStore || storeEl.lnStore) : null,
209
+ connector: connectorEl ? (connectorEl.lnConnector || connectorEl.lnApiConnector || connectorEl.lnCouchDbConnector) : null,
210
+ queue: queueEl ? queueEl.lnApiQueue : null
211
+ };
212
+ };
213
+
214
+ // ─── Form Write Intake (native submit, claimed via preventDefault) ──
215
+
216
+ _component.prototype._handleSubmitRecord = function (detail) {
217
+ const children = this.findChildren();
218
+ if (!children.storeEl) {
219
+ console.warn('[ln-data-coordinator] form submit claimed but no [data-ln-data-store] child found in "' + (this._name || '') + '"');
220
+ return;
221
+ }
222
+
223
+ const raw = detail.data || {};
224
+ const id = raw.id;
225
+ const expectedVersion = raw.expected_version;
226
+ const data = Object.assign({}, raw);
227
+ delete data.id;
228
+ delete data.expected_version;
229
+
230
+ const method = detail.method.toUpperCase();
231
+
232
+ if (method === 'POST') {
233
+ this._fanOutCreate(children, data, detail.action);
234
+ } else if (method === 'PUT' || method === 'PATCH') {
235
+ this._fanOutUpdate(children, id, data, expectedVersion, detail.action);
236
+ }
237
+ };
238
+
239
+ // ─── Parallel Fan-Out (local store write + remote connector/queue) ──────
240
+
241
+ _component.prototype._fanOutCreate = function (children, data, action) {
242
+ this.refreshMapper();
243
+ const tempId = '_temp_' + _uuid();
244
+
245
+ dispatch(children.storeEl, 'ln-data-store:request-create', { tempId: tempId, data: data });
246
+
247
+ if (children.queue) {
248
+ dispatch(children.queueEl, 'ln-api-queue:request-enqueue', {
249
+ chainKey: tempId, op: 'create', targetId: null,
250
+ payload: this.mapper.egress(data), expectedVersion: null,
251
+ meta: { tempId: tempId, action: action }
252
+ });
253
+ } else if (children.connector) {
254
+ dispatch(children.connectorEl, 'ln-api-connector:request-create', {
255
+ data: this.mapper.egress(data), url: action,
256
+ meta: { entryId: _uuid(), queued: false, op: 'create', tempId: tempId }
257
+ });
258
+ }
259
+ };
260
+
261
+ _component.prototype._fanOutUpdate = function (children, id, data, expectedVersion, action) {
262
+ this.refreshMapper();
263
+
264
+ dispatch(children.storeEl, 'ln-data-store:request-update', { id: id, data: data });
265
+
266
+ if (children.queue) {
267
+ dispatch(children.queueEl, 'ln-api-queue:request-enqueue', {
268
+ chainKey: id, op: 'update', targetId: id,
269
+ payload: this.mapper.egress(data), expectedVersion: expectedVersion,
270
+ meta: { id: id, action: action }
271
+ });
272
+ } else if (children.connector) {
273
+ dispatch(children.connectorEl, 'ln-api-connector:request-update', {
274
+ id: id, data: this.mapper.egress(data), expected_version: expectedVersion, url: action,
275
+ meta: { entryId: _uuid(), queued: false, op: 'update', id: id }
276
+ });
277
+ }
278
+ };
279
+
280
+ _component.prototype._fanOutDelete = function (children, id) {
281
+ this.refreshMapper();
282
+
283
+ dispatch(children.storeEl, 'ln-data-store:request-delete', { id: id });
284
+
285
+ if (children.queue) {
286
+ dispatch(children.queueEl, 'ln-api-queue:request-enqueue', {
287
+ chainKey: id, op: 'delete', targetId: id, payload: null, expectedVersion: null, meta: { id: id }
288
+ });
289
+ } else if (children.connector) {
290
+ dispatch(children.connectorEl, 'ln-api-connector:request-delete', {
291
+ id: id, meta: { entryId: _uuid(), queued: false, op: 'delete', id: id }
292
+ });
293
+ }
294
+ };
295
+
296
+ _component.prototype._fanOutBulkDelete = function (children, ids) {
297
+ this.refreshMapper();
298
+ const bulkKey = ids.join(',');
299
+
300
+ dispatch(children.storeEl, 'ln-data-store:request-bulk-delete', { ids: ids });
301
+
302
+ if (children.queue) {
303
+ dispatch(children.queueEl, 'ln-api-queue:request-enqueue', {
304
+ chainKey: bulkKey, op: 'bulk-delete', targetId: null, payload: { ids: ids }, expectedVersion: null, meta: { bulkKey: bulkKey, ids: ids }
305
+ });
306
+ } else if (children.connector) {
307
+ dispatch(children.connectorEl, 'ln-api-connector:request-bulk-delete', {
308
+ ids: ids, meta: { entryId: _uuid(), queued: false, op: 'bulk-delete', bulkKey: bulkKey }
309
+ });
310
+ }
311
+ };
312
+
313
+ // ─── Toast Helpers ────────────────────────────────────────
314
+
315
+ _component.prototype._toastFromMessage = function (message) {
316
+ if (!message) return;
317
+ window.dispatchEvent(new CustomEvent('ln-toast:enqueue', {
318
+ detail: {
319
+ type: message.type || 'success',
320
+ title: message.title || '',
321
+ message: message.body || ''
322
+ }
323
+ }));
324
+ };
325
+
326
+ _component.prototype._toastFromDict = function (key) {
327
+ const text = this._dict[key];
328
+ if (!text) return;
329
+ window.dispatchEvent(new CustomEvent('ln-toast:enqueue', {
330
+ detail: { type: 'error', title: '', message: text }
331
+ }));
332
+ };
333
+
334
+ _component.prototype._requestStoreMutation = function (children, action, detail) {
335
+ const storeEl = children.storeEl;
336
+ if (!storeEl) return Promise.reject(new Error('Store element not found'));
337
+
338
+ const requestId = _uuid();
339
+ const receipt = this._mutationReceipts.wait(requestId);
340
+ try {
341
+ dispatch(storeEl, 'ln-data-store:request-' + action, Object.assign({}, detail, { requestId }));
342
+ } catch (error) {
343
+ this._mutationReceipts.reject({ requestId, error });
344
+ }
345
+ return receipt;
346
+ };
347
+
348
+ _component.prototype._reportReconciliationError = function (operation, error, meta) {
349
+ dispatch(this.dom, 'ln-data-coordinator:error', {
350
+ operation,
351
+ error,
352
+ meta: meta || null
353
+ });
354
+ };
355
+
356
+ // ─── Event Binding ────────────────────────────────────────
357
+
358
+ function _bindEvents(self) {
359
+ self._handlers = {
360
+ sync: function (e) {
361
+ self.refreshMapper();
362
+ const children = self.findChildren();
363
+ if (!children.store || !children.connector) {
364
+ console.warn('[ln-data-coordinator] Cannot sync: store or connector not found in subtree');
365
+ return;
366
+ }
367
+ dispatch(children.connectorEl, 'ln-api-connector:request-sync', { since: e.detail.since, meta: { op: 'sync' } });
368
+ },
369
+
370
+ reqCreate: function (e) {
371
+ const children = self.findChildren();
372
+ if (!children.storeEl) return;
373
+ self._fanOutCreate(children, e.detail.data || {}, e.detail.action);
374
+ },
375
+
376
+ reqUpdate: function (e) {
377
+ const children = self.findChildren();
378
+ if (!children.storeEl) return;
379
+ self._fanOutUpdate(children, e.detail.id, e.detail.data || {}, e.detail.expected_version, e.detail.action);
380
+ },
381
+
382
+ reqDelete: function (e) {
383
+ const children = self.findChildren();
384
+ if (!children.storeEl) return;
385
+ self._fanOutDelete(children, e.detail.id);
386
+ },
387
+
388
+ reqBulkDelete: function (e) {
389
+ const children = self.findChildren();
390
+ if (!children.storeEl) return;
391
+ self._fanOutBulkDelete(children, e.detail.ids || []);
392
+ },
393
+
394
+ queueFailed: function () {
395
+ self._toastFromDict('network');
396
+ },
397
+
398
+ // ─── Queue Transport Executor ─────────────────────────
399
+ queueSend: function (e) {
400
+ self.refreshMapper();
401
+ const children = self.findChildren();
402
+ if (!children.store || !children.connector || !children.queue) return;
403
+
404
+ const detail = e.detail || {};
405
+ const entryId = detail.entryId;
406
+ const op = detail.op;
407
+ const targetId = detail.targetId;
408
+ const payload = detail.payload;
409
+ const expectedVersion = detail.expectedVersion;
410
+ const queueMeta = detail.meta || {};
411
+ const resourceUrl = queueMeta.action || null;
412
+ const idempotencyKey = detail.idempotencyKey || entryId;
413
+
414
+ if (op === 'create') {
415
+ dispatch(children.connectorEl, 'ln-api-connector:request-create', {
416
+ data: payload, url: resourceUrl, idempotencyKey: idempotencyKey,
417
+ meta: { entryId: entryId, queued: true, op: 'create', tempId: queueMeta.tempId }
418
+ });
419
+ } else if (op === 'update') {
420
+ dispatch(children.connectorEl, 'ln-api-connector:request-update', {
421
+ id: targetId, data: payload, expected_version: expectedVersion, url: resourceUrl, idempotencyKey: idempotencyKey,
422
+ meta: { entryId: entryId, queued: true, op: 'update', id: targetId }
423
+ });
424
+ } else if (op === 'delete') {
425
+ dispatch(children.connectorEl, 'ln-api-connector:request-delete', {
426
+ id: targetId, idempotencyKey: idempotencyKey,
427
+ meta: { entryId: entryId, queued: true, op: 'delete', id: targetId }
428
+ });
429
+ } else if (op === 'bulk-delete') {
430
+ dispatch(children.connectorEl, 'ln-api-connector:request-bulk-delete', {
431
+ ids: (payload && payload.ids) ? payload.ids : [],
432
+ idempotencyKey: idempotencyKey,
433
+ meta: { entryId: entryId, queued: true, op: 'bulk-delete', bulkKey: queueMeta.bulkKey }
434
+ });
435
+ } else {
436
+ console.warn('[ln-data-coordinator] Unknown queue op:', op);
437
+ }
438
+ },
439
+
440
+ // ─── Form Write Intake — native submit, bubble phase ──────
441
+ formSubmit: function (e) {
442
+ const form = e.target;
443
+ if (e.defaultPrevented) return; // ln-validate's submit gate blocked it, or another coordinator already claimed it
444
+
445
+ const scopeAttr = form.hasAttribute(SCOPE_ATTR) ? form.getAttribute(SCOPE_ATTR) : null;
446
+ if (scopeAttr === null) return; // form never opted in — leave native submit alone
447
+
448
+ let isMine;
449
+ if (scopeAttr) {
450
+ isMine = (scopeAttr === self._name);
451
+ } else {
452
+ isMine = (form.closest('[data-ln-data-coordinator]') === self.dom);
453
+ }
454
+ if (!isMine) return;
455
+
456
+ const method = resolveFormMethod(form);
457
+ if (method !== 'POST' && method !== 'PUT' && method !== 'PATCH') return;
458
+
459
+ e.preventDefault(); // claim
460
+
461
+ const raw = serializeForm(form);
462
+ delete raw._method;
463
+ delete raw._token;
464
+
465
+ self._handleSubmitRecord({ data: raw, method: method, action: form.getAttribute('action') || '' });
466
+ },
467
+
468
+ // ─── Connector Response Handlers (direct + queued paths) ──
469
+ connFetched: function (e) {
470
+ const meta = e.detail.meta || {};
471
+ const children = self.findChildren();
472
+
473
+ self.refreshMapper();
474
+ const rawResponse = e.detail.data;
475
+ let fetchedRecords = [], deletedIds = [], syncedAt = null;
476
+
477
+ if (Array.isArray(rawResponse)) {
478
+ fetchedRecords = rawResponse;
479
+ syncedAt = Math.floor(Date.now() / 1000);
480
+ } else if (rawResponse) {
481
+ fetchedRecords = Array.isArray(rawResponse.data) ? rawResponse.data : [];
482
+ deletedIds = Array.isArray(rawResponse.deleted) ? rawResponse.deleted : [];
483
+ syncedAt = rawResponse.synced_at !== undefined ? rawResponse.synced_at : (rawResponse.since !== undefined ? rawResponse.since : null);
484
+ }
485
+
486
+ const normalizedData = fetchedRecords.map(r => self.mapper.ingress(r));
487
+
488
+ // 1. Pass fetched remote data strictly to ln-data-store (Single Source of Truth)
489
+ if (children.store && !children.store.initializationError) {
490
+ if (meta.kind) {
491
+ if (meta.kind === 'table' || meta.kind === 'list') {
492
+ children.store.applyQuery(normalizedData, { total: e.detail.total }).then(function (decorated) {
493
+ dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-loading', { loading: false });
494
+ dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-data', {
495
+ data: decorated,
496
+ total: e.detail.total !== undefined ? e.detail.total : decorated.length,
497
+ filtered: e.detail.filtered !== undefined ? e.detail.filtered : decorated.length,
498
+ offset: e.detail.offset,
499
+ queryGen: e.detail.queryGen
500
+ });
501
+ self._boundDelivered.set(meta.targetEl, true);
502
+ });
503
+ } else if (meta.kind === 'options') {
504
+ children.store.applyQuery(normalizedData, { total: e.detail.total }).then(function () {
505
+ return children.store.getAll({});
506
+ }).then(function (r) {
507
+ dispatch(meta.targetEl, 'ln-options:set-data', { data: r.data });
508
+ });
509
+ } else if (meta.kind === 'stat') {
510
+ children.store.applyQuery(normalizedData, { total: e.detail.total }).then(function () {
511
+ const count = e.detail.filtered !== undefined
512
+ ? e.detail.filtered
513
+ : (e.detail.total !== undefined ? e.detail.total : normalizedData.length);
514
+ dispatch(meta.targetEl, 'ln-stat:set-count', { count: count });
515
+ });
516
+ }
517
+ } else {
518
+ children.store.applySync(normalizedData, deletedIds, syncedAt || Math.floor(Date.now() / 1000), {
519
+ total: e.detail.total,
520
+ filtered: e.detail.filtered,
521
+ offset: e.detail.offset,
522
+ queryGen: e.detail.queryGen,
523
+ targetEl: meta.targetEl
524
+ });
525
+ }
526
+ } else if (meta.targetEl && meta.kind) {
527
+ if (meta.kind === 'table' || meta.kind === 'list') {
528
+ dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-loading', { loading: false });
529
+ dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-data', {
530
+ data: normalizedData,
531
+ total: e.detail.total !== undefined ? e.detail.total : normalizedData.length,
532
+ filtered: e.detail.filtered !== undefined ? e.detail.filtered : normalizedData.length,
533
+ offset: e.detail.offset,
534
+ queryGen: e.detail.queryGen
535
+ });
536
+ self._boundDelivered.set(meta.targetEl, true);
537
+ } else if (meta.kind === 'options') {
538
+ dispatch(meta.targetEl, 'ln-options:set-data', { data: normalizedData });
539
+ } else if (meta.kind === 'stat') {
540
+ const count = e.detail.filtered !== undefined
541
+ ? e.detail.filtered
542
+ : (e.detail.total !== undefined ? e.detail.total : normalizedData.length);
543
+ dispatch(meta.targetEl, 'ln-stat:set-count', { count: count });
544
+ }
545
+ }
546
+ },
547
+
548
+ connCreated: function (e) {
549
+ const children = self.findChildren();
550
+ if (!children.storeEl) return;
551
+ const meta = e.detail.meta || {};
552
+ const serverRecord = self.mapper.ingress(e.detail.record);
553
+
554
+ self._requestStoreMutation(children, 'update', { id: meta.tempId, data: serverRecord })
555
+ .then(function () {
556
+ self._toastFromMessage(e.detail.message);
557
+ if (meta.queued && children.queue) {
558
+ dispatch(children.queueEl, 'ln-api-queue:resolve-create', {
559
+ entryId: meta.entryId,
560
+ oldKey: meta.tempId,
561
+ newId: serverRecord.id
562
+ });
563
+ }
564
+ })
565
+ .catch(function (error) {
566
+ self._reportReconciliationError('create-reconcile', error, meta);
567
+ });
568
+ },
569
+
570
+ connUpdated: function (e) {
571
+ const children = self.findChildren();
572
+ if (!children.storeEl) return;
573
+ const meta = e.detail.meta || {};
574
+ const serverRecord = self.mapper.ingress(e.detail.record);
575
+
576
+ self._requestStoreMutation(children, 'update', { id: meta.id, data: serverRecord })
577
+ .then(function () {
578
+ self._toastFromMessage(e.detail.message);
579
+ if (meta.queued && children.queue) {
580
+ dispatch(children.queueEl, 'ln-api-queue:ack', { entryId: meta.entryId });
581
+ }
582
+ })
583
+ .catch(function (error) {
584
+ self._reportReconciliationError('update-reconcile', error, meta);
585
+ });
586
+ },
587
+
588
+ connDeleted: function (e) {
589
+ const children = self.findChildren();
590
+ if (!children.storeEl) return;
591
+ const meta = e.detail.meta || {};
592
+ // Optimistic delete already applied; no local reconciliation.
593
+ self._toastFromMessage(e.detail.message); // null on 204 → silent
594
+ if (meta.queued && children.queue) {
595
+ dispatch(children.queueEl, 'ln-api-queue:ack', { entryId: meta.entryId });
596
+ }
597
+ },
598
+
599
+ connBulkDeleted: function (e) {
600
+ const children = self.findChildren();
601
+ if (!children.storeEl) return;
602
+ const meta = e.detail.meta || {};
603
+ self._toastFromMessage(e.detail.message);
604
+ if (meta.queued && children.queue) {
605
+ dispatch(children.queueEl, 'ln-api-queue:ack', { entryId: meta.entryId });
606
+ }
607
+ },
608
+
609
+ connError: function (e) {
610
+ const detail = e.detail || {};
611
+ const meta = detail.meta || {};
612
+ const op = meta.op || detail.action;
613
+ const status = detail.status || 0;
614
+ const children = self.findChildren();
615
+
616
+ if (op === 'sync') {
617
+ if (children.storeEl) {
618
+ dispatch(children.storeEl, 'ln-data-store:request-sync-failed', {
619
+ error: detail.error,
620
+ status: status
621
+ });
622
+ }
623
+ console.error('[ln-data-coordinator] Sync failed:', detail.error);
624
+ return;
625
+ }
626
+
627
+ if (op === 'query') {
628
+ if (meta.targetEl && meta.kind) {
629
+ dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-loading', { loading: false });
630
+ if (meta.kind === 'table' || meta.kind === 'list') {
631
+ dispatch(meta.targetEl, 'ln-' + meta.kind + ':page-failed', { offset: meta.offset });
632
+ }
633
+ }
634
+ self._reportReconciliationError('query', detail.error || detail, meta);
635
+ return;
636
+ }
637
+
638
+ if (!children.storeEl) return;
639
+
640
+ const isAuth = status === 401 || status === 419;
641
+ const isTransient = status === 0 || status >= 500;
642
+ const isConflict = status === 409;
643
+
644
+ // ── Auth: pause queue, keep local write ──
645
+ if (isAuth) {
646
+ self._toastFromDict('auth');
647
+ if (meta.queued && children.queue) {
648
+ dispatch(children.queueEl, 'ln-api-queue:nack', { entryId: meta.entryId, reason: 'auth' });
649
+ }
650
+ return;
651
+ }
652
+
653
+ // ── Transient (5xx / network / 0): NEVER delete local ──
654
+ if (isTransient) {
655
+ if (meta.queued && children.queue) {
656
+ // Retry via queue ladder; toast deferred to ln-api-queue:failed
657
+ dispatch(children.queueEl, 'ln-api-queue:nack', { entryId: meta.entryId, reason: 'retry' });
658
+ } else {
659
+ // No queue: single attempt spent; record stays local, surface now
660
+ self._toastFromDict('network');
661
+ }
662
+ return;
663
+ }
664
+
665
+ // ── Deterministic (4xx / 3xx): never retry ──
666
+ let reconciliation = Promise.resolve();
667
+ if (isConflict && op === 'update') {
668
+ const remote = detail.data && detail.data.remote ? self.mapper.ingress(detail.data.remote) : null;
669
+ if (remote) {
670
+ reconciliation = self._requestStoreMutation(children, 'update', { id: meta.id, data: remote });
671
+ }
672
+ self._toastFromDict('conflict');
673
+ } else if (op === 'create') {
674
+ reconciliation = self._requestStoreMutation(children, 'delete', { id: meta.tempId });
675
+ self._toastFromDict('rejected');
676
+ } else {
677
+ // update/delete/bulk generic 4xx (incl. 404): leave local, next sync reconciles
678
+ self._toastFromDict('rejected');
679
+ }
680
+
681
+ if (meta.queued && children.queue) {
682
+ reconciliation.then(function () {
683
+ dispatch(children.queueEl, 'ln-api-queue:nack', { entryId: meta.entryId, reason: 'drop' });
684
+ }).catch(function (error) {
685
+ self._reportReconciliationError('deterministic-reconcile', error, meta);
686
+ });
687
+ } else {
688
+ reconciliation.catch(function (error) {
689
+ self._reportReconciliationError('deterministic-reconcile', error, meta);
690
+ });
691
+ }
692
+ },
693
+
694
+ // ─── Store Initialized (Sync Ownership) ───────────────
695
+ storeInitialized: function (e) {
696
+ const children = self.findChildren();
697
+ const store = children.store;
698
+ if (!store || store.initializationError || !children.connector || self._noAutosync || store.isSyncing) return;
699
+
700
+ const detail = e.detail || {};
701
+ if (!detail.hasCache) {
702
+ store.forceSync();
703
+ } else if (self._isStale()) {
704
+ store.forceSync();
705
+ }
706
+ },
707
+
708
+ // ─── View Binder Handlers ─────────────────────────────
709
+ reqTableData: function (e) { self._serveData(e, 'table'); },
710
+ reqListData: function (e) { self._serveData(e, 'list'); },
711
+ reqOptions: function (e) { self._serveOptions(e); },
712
+ reqStat: function (e) { self._serveStat(e); },
713
+ refresh: function (e) {
714
+ self._mutationReceipts.resolve(e.detail);
715
+ self._refreshAll();
716
+ },
717
+ mutationError: function (e) {
718
+ self._mutationReceipts.reject(e.detail);
719
+ },
720
+ refreshSynced: function (e) {
721
+ if (e.detail && e.detail.changed) self._refreshAll(e.detail.meta);
722
+ }
723
+ };
724
+
725
+ // Sync request bubbling up from the child store
726
+ self.dom.addEventListener('ln-data-store:request-remote-sync', self._handlers.sync);
727
+
728
+ // Coordinator-namespaced intake events (parallel fan-out)
729
+ self.dom.addEventListener('ln-data-coordinator:request-create', self._handlers.reqCreate);
730
+ self.dom.addEventListener('ln-data-coordinator:request-update', self._handlers.reqUpdate);
731
+ self.dom.addEventListener('ln-data-coordinator:request-delete', self._handlers.reqDelete);
732
+ self.dom.addEventListener('ln-data-coordinator:request-bulk-delete', self._handlers.reqBulkDelete);
733
+
734
+ // Queue transport executor + terminal failure
735
+ self.dom.addEventListener('ln-api-queue:send', self._handlers.queueSend);
736
+ self.dom.addEventListener('ln-api-queue:failed', self._handlers.queueFailed);
737
+
738
+ // Sync ownership — store initialization
739
+ self.dom.addEventListener('ln-data-store:initialized', self._handlers.storeInitialized);
740
+
741
+ // Form write intake — native submit, document-level, bubble phase (never
742
+ // capture: ln-validate's own submit gate on the form must run first)
743
+ document.addEventListener('submit', self._handlers.formSubmit);
744
+
745
+ // Connector responses — generalized across concrete connector implementations
746
+ CONNECTOR_RESPONSE_NAMESPACES.forEach(function (ns) {
747
+ self.dom.addEventListener(ns + ':fetched', self._handlers.connFetched);
748
+ self.dom.addEventListener(ns + ':created', self._handlers.connCreated);
749
+ self.dom.addEventListener(ns + ':updated', self._handlers.connUpdated);
750
+ self.dom.addEventListener(ns + ':deleted', self._handlers.connDeleted);
751
+ self.dom.addEventListener(ns + ':bulk-deleted', self._handlers.connBulkDeleted);
752
+ self.dom.addEventListener(ns + ':error', self._handlers.connError);
753
+ });
754
+
755
+ // View binder — request handlers (document-level to reach tables/lists outside this subtree)
756
+ document.addEventListener('ln-table:request-data', self._handlers.reqTableData);
757
+ document.addEventListener('ln-list:request-data', self._handlers.reqListData);
758
+ document.addEventListener('ln-options:request-data', self._handlers.reqOptions);
759
+ document.addEventListener('ln-stat:request-count', self._handlers.reqStat);
760
+
761
+ // Store-change refresh — attach to self.dom so bubbling store events are caught
762
+ self.dom.addEventListener('ln-data-store:ready', self._handlers.refresh);
763
+ self.dom.addEventListener('ln-data-store:created', self._handlers.refresh);
764
+ self.dom.addEventListener('ln-data-store:updated', self._handlers.refresh);
765
+ self.dom.addEventListener('ln-data-store:deleted', self._handlers.refresh);
766
+ self.dom.addEventListener('ln-data-store:mutation-error', self._handlers.mutationError);
767
+ self.dom.addEventListener('ln-data-store:synced', self._handlers.refreshSynced);
768
+ }
769
+
770
+ // ─── Store↔View Binder ───────────────────────────────────
771
+
772
+ _component.prototype._ownsStore = function (name) {
773
+ const children = this.findChildren();
774
+ if (children.store && children.store._name === name && name) return true;
775
+ if (this._name === name && name) return true;
776
+ return false;
777
+ };
778
+
779
+ _component.prototype._serveData = function (e, kind) {
780
+ const el = e.target;
781
+ const attrName = kind === 'table' ? 'data-ln-table-store' : 'data-ln-list-store';
782
+ const storeName = el.getAttribute(attrName) || el.getAttribute('data-ln-table-source') || el.getAttribute('data-ln-list-source');
783
+ if (!storeName) return;
784
+ if (!this._ownsStore(storeName)) return;
785
+
786
+ const request = e.detail || {};
787
+ const query = normalizeDataQuery(request);
788
+ this._boundQueries.set(el, query);
789
+
790
+ const children = this.findChildren();
791
+ const self = this;
792
+ const isWindowed = request.offset != null;
793
+ const store = children.store;
794
+ const ready = store && store.ready ? store.ready : Promise.resolve();
795
+
796
+ return ready.then(function () {
797
+ const source = selectDataSource(store, children.connector, isWindowed);
798
+ if (source === 'remote') {
799
+ dispatch(el, 'ln-' + kind + ':set-loading', { loading: true });
800
+ dispatch(children.connectorEl, 'ln-api-connector:request-query', {
801
+ query: query,
802
+ meta: { targetEl: el, kind: kind, offset: query.offset, limit: query.limit }
803
+ });
804
+ return;
805
+ }
806
+
807
+ if (source !== 'store') {
808
+ dispatch(el, 'ln-' + kind + ':set-loading', { loading: false });
809
+ return;
810
+ }
811
+
812
+ return store.getAll(query).then(function (r) {
813
+ const detail = {
814
+ data: r.data,
815
+ total: r.total,
816
+ filtered: r.filtered,
817
+ offset: request.offset !== undefined ? request.offset : r.offset,
818
+ queryGen: request.queryGen !== undefined ? request.queryGen : r.queryGen
819
+ };
820
+ dispatch(el, 'ln-' + kind + ':set-data', detail);
821
+ self._boundDelivered.set(el, true);
822
+ });
823
+ }).catch(function (error) {
824
+ dispatch(el, 'ln-' + kind + ':set-loading', { loading: false });
825
+ dispatch(self.dom, 'ln-data-coordinator:error', {
826
+ operation: 'query',
827
+ kind: kind,
828
+ store: storeName,
829
+ target: el,
830
+ error: error
831
+ });
832
+ });
833
+ };
834
+
835
+ _component.prototype._serveOptions = function (e) {
836
+ const el = e.target;
837
+ const name = el.getAttribute('data-ln-options');
838
+ if (!this._ownsStore(name)) return;
839
+
840
+ const children = this.findChildren();
841
+ const store = children.store;
842
+ const ready = store && store.ready ? store.ready : Promise.resolve();
843
+ const self = this;
844
+
845
+ return ready.then(function () {
846
+ const source = selectDataSource(store, children.connector, false);
847
+ if (source === 'remote') {
848
+ dispatch(children.connectorEl, 'ln-api-connector:request-query', {
849
+ query: {},
850
+ meta: { targetEl: el, kind: 'options' }
851
+ });
852
+ return;
853
+ }
854
+ if (source !== 'store') return;
855
+ return store.getAll({}).then(function (r) {
856
+ dispatch(el, 'ln-options:set-data', { data: r.data });
857
+ });
858
+ }).catch(function (error) {
859
+ self._reportReconciliationError('options-query', error, { targetEl: el, kind: 'options' });
860
+ });
861
+ };
862
+
863
+ _component.prototype._serveStat = function (e) {
864
+ const el = e.target;
865
+ const name = el.getAttribute('data-ln-stat');
866
+ if (!this._ownsStore(name)) return;
867
+
868
+ const filters = e.detail && e.detail.filters ? e.detail.filters : null;
869
+ const children = this.findChildren();
870
+ const store = children.store;
871
+ const ready = store && store.ready ? store.ready : Promise.resolve();
872
+ const self = this;
873
+
874
+ return ready.then(function () {
875
+ const source = selectDataSource(store, children.connector, false);
876
+ if (source === 'remote') {
877
+ dispatch(children.connectorEl, 'ln-api-connector:request-query', {
878
+ query: { filters: filters },
879
+ meta: { targetEl: el, kind: 'stat' }
880
+ });
881
+ return;
882
+ }
883
+ if (source !== 'store') return;
884
+ return store.count(filters).then(function (n) {
885
+ dispatch(el, 'ln-stat:set-count', { count: n });
886
+ });
887
+ }).catch(function (error) {
888
+ self._reportReconciliationError('stat-query', error, { targetEl: el, kind: 'stat' });
889
+ });
890
+ };
891
+
892
+ _component.prototype._refreshAll = function (syncMeta) {
893
+ const self = this;
894
+ const allBound = document.querySelectorAll('[data-ln-table-store],[data-ln-list-store],[data-ln-options],[data-ln-stat]');
895
+ for (let i = 0; i < allBound.length; i++) {
896
+ const el = allBound[i];
897
+ let storeName, kind;
898
+
899
+ if (el.hasAttribute('data-ln-table-store')) {
900
+ storeName = el.getAttribute('data-ln-table-store');
901
+ kind = 'table';
902
+ } else if (el.hasAttribute('data-ln-list-store')) {
903
+ storeName = el.getAttribute('data-ln-list-store');
904
+ kind = 'list';
905
+ } else if (el.hasAttribute('data-ln-options')) {
906
+ storeName = el.getAttribute('data-ln-options');
907
+ kind = 'options';
908
+ } else if (el.hasAttribute('data-ln-stat')) {
909
+ storeName = el.getAttribute('data-ln-stat');
910
+ kind = 'stat';
911
+ }
912
+
913
+ if (!this._ownsStore(storeName)) continue;
914
+
915
+ const store = this.findChildren().store;
916
+
917
+ if (kind === 'table' || kind === 'list') {
918
+ const windowAttr = kind === 'table' ? 'data-ln-table-window' : 'data-ln-list-window';
919
+ if (el.hasAttribute(windowAttr)) {
920
+ dispatch(el, 'ln-' + kind + ':request-revalidate', {});
921
+ continue;
922
+ }
923
+ const cached = self._boundQueries.get(el) || { sort: null, filters: {}, search: '' };
924
+ (function (capturedEl, capturedKind) {
925
+ store.getAll(cached).then(function (r) {
926
+ const detail = {
927
+ data: r.data,
928
+ total: (syncMeta && syncMeta.total !== undefined) ? syncMeta.total : r.total,
929
+ filtered: (syncMeta && syncMeta.filtered !== undefined) ? syncMeta.filtered : r.filtered,
930
+ offset: (syncMeta && syncMeta.offset !== undefined) ? syncMeta.offset : cached.offset,
931
+ queryGen: (syncMeta && syncMeta.queryGen !== undefined) ? syncMeta.queryGen : cached.queryGen
932
+ };
933
+ dispatch(capturedEl, 'ln-' + capturedKind + ':set-loading', { loading: false });
934
+ dispatch(capturedEl, 'ln-' + capturedKind + ':set-data', detail);
935
+ self._boundDelivered.set(capturedEl, true);
936
+ });
937
+ })(el, kind);
938
+ } else if (kind === 'options') {
939
+ (function (capturedEl) {
940
+ store.getAll({}).then(function (r) {
941
+ dispatch(capturedEl, 'ln-options:set-data', { data: r.data });
942
+ });
943
+ })(el);
944
+ } else if (kind === 'stat') {
945
+ const raw = el.getAttribute('data-ln-stat-filter');
946
+ let filters = null;
947
+ if (raw) {
948
+ const colonIdx = raw.indexOf(':');
949
+ if (colonIdx !== -1) {
950
+ const field = raw.slice(0, colonIdx);
951
+ const val = raw.slice(colonIdx + 1);
952
+ filters = {};
953
+ filters[field] = [val];
954
+ }
955
+ }
956
+ (function (capturedEl, capturedFilters) {
957
+ store.count(capturedFilters).then(function (n) {
958
+ dispatch(capturedEl, 'ln-stat:set-count', { count: n });
959
+ });
960
+ })(el, filters);
961
+ }
962
+ }
963
+ };
964
+
965
+ // ─── Destroy and Cleanup ──────────────────────────────────
966
+
967
+ _component.prototype.destroy = function () {
968
+ if (!this.dom[DOM_ATTRIBUTE]) return;
969
+
970
+ const self = this;
971
+ if (self._handlers) {
972
+ self.dom.removeEventListener('ln-data-store:request-remote-sync', self._handlers.sync);
973
+
974
+ self.dom.removeEventListener('ln-data-coordinator:request-create', self._handlers.reqCreate);
975
+ self.dom.removeEventListener('ln-data-coordinator:request-update', self._handlers.reqUpdate);
976
+ self.dom.removeEventListener('ln-data-coordinator:request-delete', self._handlers.reqDelete);
977
+ self.dom.removeEventListener('ln-data-coordinator:request-bulk-delete', self._handlers.reqBulkDelete);
978
+
979
+ self.dom.removeEventListener('ln-api-queue:send', self._handlers.queueSend);
980
+ self.dom.removeEventListener('ln-api-queue:failed', self._handlers.queueFailed);
981
+ self.dom.removeEventListener('ln-data-store:initialized', self._handlers.storeInitialized);
982
+
983
+ document.removeEventListener('submit', self._handlers.formSubmit);
984
+
985
+ CONNECTOR_RESPONSE_NAMESPACES.forEach(function (ns) {
986
+ self.dom.removeEventListener(ns + ':fetched', self._handlers.connFetched);
987
+ self.dom.removeEventListener(ns + ':created', self._handlers.connCreated);
988
+ self.dom.removeEventListener(ns + ':updated', self._handlers.connUpdated);
989
+ self.dom.removeEventListener(ns + ':deleted', self._handlers.connDeleted);
990
+ self.dom.removeEventListener(ns + ':bulk-deleted', self._handlers.connBulkDeleted);
991
+ self.dom.removeEventListener(ns + ':error', self._handlers.connError);
992
+ });
993
+
994
+ // View binder — document-level listeners
995
+ document.removeEventListener('ln-table:request-data', self._handlers.reqTableData);
996
+ document.removeEventListener('ln-list:request-data', self._handlers.reqListData);
997
+ document.removeEventListener('ln-options:request-data', self._handlers.reqOptions);
998
+ document.removeEventListener('ln-stat:request-count', self._handlers.reqStat);
999
+
1000
+ // Store-change listeners
1001
+ self.dom.removeEventListener('ln-data-store:ready', self._handlers.refresh);
1002
+ self.dom.removeEventListener('ln-data-store:created', self._handlers.refresh);
1003
+ self.dom.removeEventListener('ln-data-store:updated', self._handlers.refresh);
1004
+ self.dom.removeEventListener('ln-data-store:deleted', self._handlers.refresh);
1005
+ self.dom.removeEventListener('ln-data-store:mutation-error', self._handlers.mutationError);
1006
+ self.dom.removeEventListener('ln-data-store:synced', self._handlers.refreshSynced);
1007
+
1008
+ self._handlers = null;
1009
+ }
1010
+
1011
+ self._boundQueries = null;
1012
+ self._boundDelivered = null;
1013
+ self._mutationReceipts.close(new Error('Data coordinator destroyed'));
1014
+ self._mutationReceipts = null;
1015
+
1016
+ _coordinators.delete(this);
1017
+ _uninstallGlobalSync();
1018
+
1019
+ delete this.dom[DOM_ATTRIBUTE];
1020
+ delete this.dom[DOM_ALIAS];
1021
+ };
1022
+
1023
+ // ─── Attribute Sync ────────────────────────────────────────
1024
+
1025
+ function _syncAttribute(el, attrName) {
1026
+ const instance = el[DOM_ATTRIBUTE];
1027
+ if (!instance) return;
1028
+
1029
+ if (attrName === 'data-ln-data-mapper') {
1030
+ instance.refreshMapper();
1031
+ }
1032
+ }
1033
+
1034
+ // ─── Registration ──────────────────────────────────────
1035
+
1036
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-data-coordinator', {
1037
+ extraAttributes: [
1038
+ 'data-ln-data-mapper'
1039
+ ],
1040
+ onAttributeChange: _syncAttribute
1041
+ });
1042
+ })();