@livenetworks/ashlar 2.0.7 → 2.0.9

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 (213) hide show
  1. package/LICENSE +641 -641
  2. package/components/COMPONENTS.md +1116 -1116
  3. package/components/index.js +81 -81
  4. package/components/ln-accordion/ln-accordion-dev.scss +8 -8
  5. package/components/ln-accordion/ln-accordion.js +1 -1
  6. package/components/ln-accordion/ln-accordion.schema.json +30 -30
  7. package/components/ln-ajax/ln-ajax-dev.scss +8 -8
  8. package/components/ln-ajax/ln-ajax.js +1 -1
  9. package/components/ln-ajax/ln-ajax.schema.json +30 -30
  10. package/components/ln-api-connector/ln-api-connector.js +1 -1
  11. package/components/ln-api-connector/ln-api-connector.schema.json +9 -0
  12. package/components/ln-api-connector/src/ln-api-connector.js +2 -0
  13. package/components/ln-api-queue/ln-api-queue.js +1 -1
  14. package/components/ln-api-queue/ln-api-queue.schema.json +34 -34
  15. package/components/ln-api-queue/src/ln-api-queue.js +3 -13
  16. package/components/ln-autoresize/ln-autoresize-dev.scss +8 -8
  17. package/components/ln-autoresize/ln-autoresize.js +1 -1
  18. package/components/ln-autoresize/ln-autoresize.schema.json +22 -22
  19. package/components/ln-autosave/ln-autosave-dev.scss +13 -13
  20. package/components/ln-autosave/ln-autosave.js +1 -1
  21. package/components/ln-autosave/ln-autosave.schema.json +48 -48
  22. package/components/ln-autosave/src/autosave-model.js +34 -34
  23. package/components/ln-chart/ln-chart.js +1 -1
  24. package/components/ln-chart/ln-chart.schema.json +146 -146
  25. package/components/ln-chart/src/chart-model.js +154 -154
  26. package/components/ln-circular-progress/ln-circular-progress.js +1 -1
  27. package/components/ln-circular-progress/ln-circular-progress.schema.json +42 -42
  28. package/components/ln-confirm/ln-confirm-dev.scss +24 -24
  29. package/components/ln-confirm/ln-confirm.js +1 -1
  30. package/components/ln-confirm/ln-confirm.schema.json +66 -66
  31. package/components/ln-confirm/src/ln-confirm.js +178 -178
  32. package/components/ln-core/attrs.js +13 -0
  33. package/components/ln-core/crypto.js +286 -265
  34. package/components/ln-core/dom.js +115 -115
  35. package/components/ln-core/form.js +146 -136
  36. package/components/ln-core/helpers.js +33 -33
  37. package/components/ln-core/http.js +48 -48
  38. package/components/ln-core/index.js +1 -1
  39. package/components/ln-core/lifecycle.js +5 -0
  40. package/components/ln-core/ln-core.schema.json +3 -2
  41. package/components/ln-core/locale.js +55 -55
  42. package/components/ln-core/matching.js +78 -78
  43. package/components/ln-core/number.js +125 -125
  44. package/components/ln-core/template.js +206 -206
  45. package/components/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  46. package/components/ln-couchdb-connector/ln-couchdb-connector.schema.json +50 -50
  47. package/components/ln-couchdb-connector/src/ln-couchdb-connector.js +1 -0
  48. package/components/ln-data-coordinator/README.md +9 -38
  49. package/components/ln-data-coordinator/ln-data-coordinator-dev.scss +8 -8
  50. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -1
  51. package/components/ln-data-coordinator/ln-data-coordinator.schema.json +16 -16
  52. package/components/ln-data-coordinator/src/data-read-policy.js +0 -7
  53. package/components/ln-data-coordinator/src/ln-data-coordinator.js +432 -1308
  54. package/components/ln-data-store/ln-data-store-dev.scss +13 -13
  55. package/components/ln-data-store/ln-data-store.js +1 -1
  56. package/components/ln-data-store/ln-data-store.schema.json +84 -84
  57. package/components/ln-data-store/src/data-store-model.js +184 -184
  58. package/components/ln-data-store/src/ln-data-store.js +1 -1
  59. package/components/ln-date/ln-date.js +1 -1
  60. package/components/ln-date/ln-date.schema.json +92 -92
  61. package/components/ln-date/src/date-model.js +186 -186
  62. package/components/ln-debug/ln-debug.js +1 -1
  63. package/components/ln-debug/ln-debug.schema.json +28 -28
  64. package/components/ln-debug/src/console-sink.js +19 -19
  65. package/components/ln-debug/src/gate.js +95 -95
  66. package/components/ln-debug/src/generated-attributes.js +4 -2
  67. package/components/ln-dropdown/ln-dropdown-dev.scss +13 -13
  68. package/components/ln-dropdown/ln-dropdown.js +1 -1
  69. package/components/ln-dropdown/ln-dropdown.schema.json +90 -90
  70. package/components/ln-editor/ln-editor.js +1 -1
  71. package/components/ln-editor/ln-editor.schema.json +58 -58
  72. package/components/ln-external-links/ln-external-links.js +1 -1
  73. package/components/ln-external-links/ln-external-links.schema.json +18 -18
  74. package/components/ln-fill/ln-fill.js +1 -1
  75. package/components/ln-fill/ln-fill.schema.json +23 -23
  76. package/components/ln-filter/ln-filter.js +1 -1
  77. package/components/ln-filter/ln-filter.schema.json +153 -153
  78. package/components/ln-filter/src/filter-model.js +185 -185
  79. package/components/ln-form/ln-form-dev.scss +8 -8
  80. package/components/ln-form/ln-form.js +1 -1
  81. package/components/ln-form/ln-form.schema.json +6 -0
  82. package/components/ln-form/src/ln-form.js +21 -0
  83. package/components/ln-http/ln-http.js +1 -1
  84. package/components/ln-http/ln-http.schema.json +6 -6
  85. package/components/ln-icon/ln-icon.schema.json +6 -6
  86. package/components/ln-include/ln-include.js +1 -1
  87. package/components/ln-include/ln-include.schema.json +23 -23
  88. package/components/ln-key/ln-key-dev.scss +18 -18
  89. package/components/ln-key/ln-key.js +1 -1
  90. package/components/ln-key/ln-key.schema.json +56 -56
  91. package/components/ln-key/src/ln-key.js +10 -10
  92. package/components/ln-link/ln-link-dev.scss +8 -8
  93. package/components/ln-link/ln-link.js +1 -1
  94. package/components/ln-link/ln-link.schema.json +22 -22
  95. package/components/ln-list/README.md +152 -152
  96. package/components/ln-list/ln-list-dev.scss +8 -8
  97. package/components/ln-list/ln-list.js +1 -1
  98. package/components/ln-list/ln-list.schema.json +166 -166
  99. package/components/ln-modal/ln-modal.js +1 -1
  100. package/components/ln-modal/ln-modal.schema.json +65 -65
  101. package/components/ln-nav/ln-nav.js +1 -1
  102. package/components/ln-nav/ln-nav.schema.json +25 -25
  103. package/components/ln-number/ln-number.js +1 -1
  104. package/components/ln-number/ln-number.schema.json +63 -63
  105. package/components/ln-number/src/ln-number.js +26 -0
  106. package/components/ln-number/src/number-model.js +31 -31
  107. package/components/ln-obfuscator/README.md +60 -60
  108. package/components/ln-obfuscator/ln-obfuscator.js +1 -1
  109. package/components/ln-obfuscator/ln-obfuscator.schema.json +49 -49
  110. package/components/ln-obfuscator/src/ln-obfuscator.js +159 -159
  111. package/components/ln-obfuscator/src/obfuscator-model.js +250 -250
  112. package/components/ln-options/ln-options.js +1 -1
  113. package/components/ln-options/ln-options.schema.json +6 -0
  114. package/components/ln-options/src/ln-options.js +4 -4
  115. package/components/ln-persist/README.md +151 -151
  116. package/components/ln-persist/ln-persist.js +1 -1
  117. package/components/ln-persist/ln-persist.schema.json +28 -28
  118. package/components/ln-persist/src/ln-persist.js +83 -83
  119. package/components/ln-picklist/README.md +230 -230
  120. package/components/ln-picklist/ln-picklist-dev.scss +22 -22
  121. package/components/ln-picklist/ln-picklist.js +1 -1
  122. package/components/ln-picklist/ln-picklist.schema.json +49 -49
  123. package/components/ln-picklist/src/ln-picklist.js +228 -228
  124. package/components/ln-popover/ln-popover-dev.scss +13 -13
  125. package/components/ln-popover/ln-popover.js +1 -1
  126. package/components/ln-popover/ln-popover.schema.json +82 -82
  127. package/components/ln-progress/ln-progress-dev.scss +8 -8
  128. package/components/ln-progress/ln-progress.js +1 -1
  129. package/components/ln-progress/ln-progress.schema.json +40 -40
  130. package/components/ln-router/ln-router.js +1 -1
  131. package/components/ln-router/ln-router.schema.json +76 -76
  132. package/components/ln-scroll/README.md +45 -45
  133. package/components/ln-scroll/ln-scroll.js +1 -1
  134. package/components/ln-scroll/ln-scroll.schema.json +85 -85
  135. package/components/ln-scroll/src/ln-scroll.js +150 -150
  136. package/components/ln-search/README.md +188 -188
  137. package/components/ln-search/ln-search.js +1 -1
  138. package/components/ln-search/ln-search.schema.json +103 -103
  139. package/components/ln-slug/ln-slug.js +1 -1
  140. package/components/ln-slug/ln-slug.schema.json +23 -23
  141. package/components/ln-slug/src/ln-slug.js +25 -1
  142. package/components/ln-sort/ln-sort.js +1 -1
  143. package/components/ln-sort/ln-sort.schema.json +104 -104
  144. package/components/ln-sort/src/ln-sort.js +321 -321
  145. package/components/ln-sort/src/sort-model.js +117 -117
  146. package/components/ln-sortable/ln-sortable-dev.scss +13 -13
  147. package/components/ln-sortable/ln-sortable.js +1 -1
  148. package/components/ln-sortable/ln-sortable.schema.json +36 -36
  149. package/components/ln-stat/ln-stat-dev.scss +8 -8
  150. package/components/ln-stat/ln-stat.js +1 -1
  151. package/components/ln-stat/ln-stat.schema.json +32 -32
  152. package/components/ln-stat/src/stat-model.js +28 -28
  153. package/components/ln-table/ln-table.js +1 -1
  154. package/components/ln-table/ln-table.schema.json +211 -211
  155. package/components/ln-table/src/table-model.js +98 -98
  156. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -1
  157. package/components/ln-table-coordinator/ln-table-coordinator.schema.json +93 -93
  158. package/components/ln-tabs/ln-tabs.js +1 -1
  159. package/components/ln-tabs/ln-tabs.schema.json +75 -75
  160. package/components/ln-tabs/src/ln-tabs.js +208 -208
  161. package/components/ln-tabs/src/tabs-model.js +82 -82
  162. package/components/ln-time/ln-time-dev.scss +13 -13
  163. package/components/ln-time/ln-time.js +1 -1
  164. package/components/ln-time/ln-time.schema.json +38 -38
  165. package/components/ln-toast/ln-toast.js +1 -1
  166. package/components/ln-toast/ln-toast.schema.json +65 -65
  167. package/components/ln-toggle/ln-toggle.js +1 -1
  168. package/components/ln-toggle/ln-toggle.schema.json +52 -52
  169. package/components/ln-tooltip/ln-tooltip.js +1 -1
  170. package/components/ln-tooltip/ln-tooltip.schema.json +86 -86
  171. package/components/ln-translations/ln-translations.js +1 -1
  172. package/components/ln-translations/ln-translations.schema.json +120 -120
  173. package/components/ln-ui-coordinator/README.md +62 -62
  174. package/components/ln-ui-coordinator/ln-ui-coordinator.js +1 -1
  175. package/components/ln-ui-coordinator/ln-ui-coordinator.schema.json +28 -28
  176. package/components/ln-ui-coordinator/src/ln-ui-coordinator.js +140 -140
  177. package/components/ln-upload/ln-upload.js +1 -1
  178. package/components/ln-upload/ln-upload.schema.json +193 -193
  179. package/components/ln-upload/src/upload-model.js +78 -78
  180. package/components/ln-validate/ln-validate.js +1 -1
  181. package/components/ln-validate/ln-validate.schema.json +50 -50
  182. package/components/ln-validate/src/validate-model.js +64 -64
  183. package/components/ln-websocket-connector/README.md +166 -166
  184. package/components/ln-websocket-connector/ln-websocket-connector.js +1 -1
  185. package/components/ln-websocket-connector/ln-websocket-connector.schema.json +29 -29
  186. package/components/ln-websocket-connector/src/ln-websocket-connector.js +283 -279
  187. package/ln-ashlar.html-data.json +15 -8
  188. package/package.json +85 -84
  189. package/theme/brand.css +73 -73
  190. package/theme/components/_confirm.scss +6 -6
  191. package/theme/components/_picklist.scss +3 -3
  192. package/theme/config/_palette.scss +274 -274
  193. package/theme/config/_theme.scss +292 -292
  194. package/theme/config/_tokens.scss +285 -285
  195. package/theme/config/mixins/_index.scss +62 -62
  196. package/theme/config/mixins/_picklist.scss +66 -66
  197. package/theme/ln-ashlar-dev.scss +66 -66
  198. package/theme/ln-ashlar-landing.scss +56 -56
  199. package/theme/ln-ashlar-theme.scss +63 -63
  200. package/theme/presentation/_bento.scss +151 -151
  201. package/theme/presentation/_cta.scss +111 -111
  202. package/theme/presentation/_effects.scss +216 -216
  203. package/theme/presentation/_features.scss +108 -108
  204. package/theme/presentation/_footer.scss +129 -129
  205. package/theme/presentation/_hero.scss +158 -158
  206. package/theme/presentation/_layout.scss +170 -170
  207. package/theme/presentation/_navbar.scss +140 -140
  208. package/theme/presentation/_pricing.scss +262 -262
  209. package/theme/presentation/_stats.scss +87 -87
  210. package/theme/presentation/_testimonials.scss +124 -124
  211. package/theme/presentation/_tokens.scss +39 -39
  212. package/theme/presentation/_typography.scss +163 -163
  213. package/web-types.json +26 -12
@@ -1,1308 +1,432 @@
1
- import { registerComponent, dispatch, buildDict, serializeForm, resolveFormMethod, createBatcher, attrSpec, defineAttrs } from '../../ln-core';
2
- import { normalizeDataQuery, selectDataSource, composeQuery, needsRemoteSupersede } 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 SCOPE_ATTR = 'data-ln-data-coordinator-scope';
9
- const SEARCH_ATTR = 'data-ln-data-coordinator-search';
10
- const FILTERS_ATTR = 'data-ln-data-coordinator-filters';
11
- const SORT_FIELD_ATTR = 'data-ln-data-coordinator-sort-field';
12
- const SORT_DIR_ATTR = 'data-ln-data-coordinator-sort-direction';
13
- if (window[DOM_ATTRIBUTE] !== undefined) return;
14
-
15
- // ─── Attribute Contract (SSOT) ──────────────────────────
16
- function _applyMapper(el) {
17
- const instance = el[DOM_ATTRIBUTE];
18
- if (!instance) return;
19
- instance.refreshMapper();
20
- }
21
-
22
- function _applyQuery(el) {
23
- const instance = el[DOM_ATTRIBUTE];
24
- if (!instance) return;
25
- instance._queueQueryRefresh();
26
- }
27
-
28
- function _readName(el, name) {
29
- return el.getAttribute(name) || el.id;
30
- }
31
-
32
- const ATTRIBUTES = {
33
- 'data-ln-data-coordinator': { prop: '_name', type: 'string', read: _readName, description: 'Coordinator name or identifier for data routing' },
34
- 'data-ln-data-coordinator-scope': { type: 'string', description: 'Scope name addressing the bound data store and connector' },
35
- 'data-ln-data-coordinator-mapper': { type: 'string', effect: _applyMapper, description: 'Name of the registered data mapper transform' },
36
- 'data-ln-data-coordinator-search': { type: 'string', effect: _applyQuery, description: 'Active search query term' },
37
- 'data-ln-data-coordinator-filters': { type: 'string', effect: _applyQuery, description: 'Active encoded filter parameters' },
38
- 'data-ln-data-coordinator-sort-field': { type: 'string', effect: _applyQuery, description: 'Active sort field property name' },
39
- 'data-ln-data-coordinator-sort-direction': { type: 'enum', values: ['asc', 'desc'], fallback: 'asc', effect: _applyQuery, description: 'Sort direction' },
40
- 'data-ln-data-coordinator-stale': { type: 'marker', description: 'Flag indicating data needs re-synchronization' },
41
- 'data-ln-data-coordinator-no-autosync': { type: 'boolean', description: 'Disables automatic synchronization upon state changes' },
42
- 'data-ln-data-coordinator-dict': { type: 'marker', description: 'Marks dictionary container for coordinator translatable messages' }
43
- };
44
-
45
- const ATTR_SPEC = attrSpec(ATTRIBUTES);
46
-
47
- // ─── Sync Orchestration Singleton ──────────────────────
48
-
49
- const _coordinators = new Set();
50
- let _globalSyncInstalled = false;
51
- let _onlineHandler = null;
52
- let _offlineHandler = null;
53
- let _visibilityHandler = null;
54
-
55
- function _installGlobalSync() {
56
- if (_globalSyncInstalled) return;
57
- _globalSyncInstalled = true;
58
-
59
- _onlineHandler = function () {
60
- dispatch(document, 'ln-data-coordinator:online', {});
61
- _coordinators.forEach(function (coord) {
62
- coord._maybeSync();
63
- });
64
- };
65
-
66
- _offlineHandler = function () {
67
- dispatch(document, 'ln-data-coordinator:offline', {});
68
- };
69
-
70
- _visibilityHandler = function () {
71
- if (document.visibilityState !== 'visible') return;
72
- _coordinators.forEach(function (coord) {
73
- const children = coord.findChildren();
74
- const store = children.store;
75
- if (store && children.connector && store.isInitialized && !store.initializationError && !store.isSyncing && !coord._noAutosync && (!store.hasCache || coord._isStale())) {
76
- store.forceSync();
77
- }
78
- });
79
- };
80
-
81
- window.addEventListener('online', _onlineHandler);
82
- window.addEventListener('offline', _offlineHandler);
83
- document.addEventListener('visibilitychange', _visibilityHandler);
84
- }
85
-
86
- function _uninstallGlobalSync() {
87
- if (!_globalSyncInstalled) return;
88
- if (_coordinators.size > 0) return;
89
-
90
- window.removeEventListener('online', _onlineHandler);
91
- window.removeEventListener('offline', _offlineHandler);
92
- document.removeEventListener('visibilitychange', _visibilityHandler);
93
-
94
- _onlineHandler = null;
95
- _offlineHandler = null;
96
- _visibilityHandler = null;
97
- _globalSyncInstalled = false;
98
- }
99
-
100
- // ─── Local Helpers ──────────────────────────────────────
101
-
102
- function _uuid() {
103
- try { return crypto.randomUUID(); }
104
- catch (_) {
105
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
106
- const r = Math.random() * 16 | 0;
107
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
108
- return v.toString(16);
109
- });
110
- }
111
- }
112
-
113
- // Connector response namespaces — generalized so writes work whether the
114
- // paired connector is ln-api-connector, ln-couchdb-connector or
115
- // ln-websocket-connector. A socket beside a REST connector only pushes;
116
- // its :fetched still arrives here.
117
- const CONNECTOR_RESPONSE_NAMESPACES = ['ln-api-connector', 'ln-couchdb-connector', 'ln-websocket-connector'];
118
-
119
- function _connectorNamespace(connectorEl) {
120
- if (!connectorEl) return 'ln-api-connector';
121
- if (connectorEl.hasAttribute('data-ln-couchdb-connector')) return 'ln-couchdb-connector';
122
- if (connectorEl.hasAttribute('data-ln-websocket-connector')) return 'ln-websocket-connector';
123
- return 'ln-api-connector';
124
- }
125
-
126
- // ─── Component Constructor ─────────────────────────────
127
-
128
- function _component(dom) {
129
- const self = this;
130
- this.dom = dom;
131
- defineAttrs(this, dom, ATTR_SPEC);
132
- if (!this._name) console.warn('[ln-data-coordinator] missing id — the coordinator cannot be addressed', dom);
133
- dom[DOM_ATTRIBUTE] = this;
134
-
135
- this._destroyed = false;
136
- this.mapper = null;
137
- this._handlers = null;
138
- this._boundQueries = new WeakMap();
139
- this._boundDelivered = new WeakMap();
140
- this._queryGens = new WeakMap();
141
- this._mutationReceipts = new MutationReceipts();
142
- this._dict = buildDict(dom, 'data-ln-data-coordinator-dict'); // flat key→string error-toast map; {} if none
143
-
144
- this._queueQueryRefresh = createBatcher(function () {
145
- if (self._destroyed) return;
146
- self._refreshAll(null, true);
147
- });
148
-
149
- this.refreshConfig();
150
- _bindEvents(this);
151
-
152
- _coordinators.add(this);
153
- _installGlobalSync();
154
-
155
- this._checkInitialSync();
156
-
157
- return this;
158
- }
159
-
160
- // ─── Observable Live Attributes (Single Source of Truth) ──
161
-
162
- Object.defineProperty(_component.prototype, '_staleThreshold', {
163
- get: function () {
164
- const children = this.findChildren();
165
- const storeEl = children.storeEl;
166
- const staleAttr = this.dom.getAttribute('data-ln-data-coordinator-stale')
167
- || (storeEl ? storeEl.getAttribute('data-ln-data-store-stale') : null);
168
- if (staleAttr === 'never' || staleAttr === '-1') return -1;
169
- const parsed = parseInt(staleAttr, 10);
170
- return isNaN(parsed) ? 300 : parsed;
171
- }
172
- });
173
-
174
- Object.defineProperty(_component.prototype, '_noAutosync', {
175
- get: function () {
176
- const children = this.findChildren();
177
- const storeEl = children.storeEl;
178
- return this.dom.hasAttribute('data-ln-data-coordinator-no-autosync')
179
- || (storeEl ? storeEl.hasAttribute('data-ln-data-store-no-autosync') : false);
180
- }
181
- });
182
-
183
- _component.prototype.refreshConfig = function () {
184
- this.refreshMapper();
185
- };
186
-
187
- _component.prototype._isStale = function () {
188
- if (this._staleThreshold === -1) return false;
189
- const children = this.findChildren();
190
- const store = children.store;
191
- if (!store || !store.lastSyncedAt) return true;
192
- const ageSeconds = (Date.now() / 1000) - store.lastSyncedAt;
193
- return ageSeconds > this._staleThreshold;
194
- };
195
-
196
- _component.prototype._maybeSync = function () {
197
- const children = this.findChildren();
198
- const store = children.store;
199
- if (!store || store.initializationError || !children.connector || this._noAutosync) return;
200
- if (!store.isInitialized || store.isSyncing) return;
201
- if (!store.hasCache || this._isStale()) store.forceSync();
202
- };
203
-
204
- // ─── Race Guard: evaluate initial sync directly at children-resolve ────
205
-
206
- _component.prototype._checkInitialSync = function () {
207
- const self = this;
208
- const initial = this.findChildren();
209
- const store = initial.store;
210
- if (!store) return;
211
-
212
- Promise.resolve(store.ready).then(function () {
213
- if (self._destroyed) return;
214
- const children = self.findChildren();
215
- const currentStore = children.store;
216
- if (currentStore && currentStore.initializationError) {
217
- self._reportReconciliationError('store-initialize', currentStore.initializationError, null);
218
- return;
219
- }
220
- if (!currentStore || !children.connector || self._noAutosync || currentStore.isSyncing) return;
221
- if (!currentStore.hasCache || self._isStale()) currentStore.forceSync();
222
- }).catch(function (error) {
223
- if (self._destroyed) return;
224
- self._reportReconciliationError('store-initialize', error, null);
225
- });
226
- };
227
-
228
- // ─── Resolve and Refresh Mapper ──────────────────────────
229
-
230
- _component.prototype.refreshMapper = function () {
231
- this.mapper = null;
232
-
233
- // 1. Check for deprecated/insecure inline script mapper
234
- const inlineScript = this.dom.querySelector('script[data-ln-mapper]');
235
- if (inlineScript) {
236
- 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.');
237
- }
238
-
239
- // 2. Resolve to registered external mapper
240
- const mapperName = this.dom.getAttribute('data-ln-data-coordinator-mapper')
241
- || this.dom.id;
242
- if (mapperName && window.lnCore && typeof window.lnCore.getDataMapper === 'function') {
243
- this.mapper = window.lnCore.getDataMapper(mapperName);
244
- }
245
-
246
- // 3. Ultimate safe fallback: no-op mapper
247
- if (!this.mapper) {
248
- this.mapper = {};
249
- }
250
-
251
- // Ensure ingress and egress are safe callable functions
252
- if (typeof this.mapper.ingress !== 'function') {
253
- this.mapper.ingress = function (r) { return r; };
254
- }
255
- if (typeof this.mapper.egress !== 'function') {
256
- this.mapper.egress = function (r) { return r; };
257
- }
258
- };
259
-
260
- // ─── Dynamic Child Discovery ──────────────────────────────
261
-
262
- _component.prototype.findChildren = function () {
263
- const storeEl = this.dom.querySelector('[data-ln-data-store]');
264
- // Requests go to a REST/CouchDB connector when there is one; a socket
265
- // takes them only when it is the sole transport.
266
- const connectorEl = this.dom.querySelector('[data-ln-api-connector], [data-ln-couchdb-connector]')
267
- || this.dom.querySelector('[data-ln-websocket-connector]');
268
- const queueEl = this.dom.querySelector('[data-ln-api-queue]');
269
-
270
- return {
271
- storeEl: storeEl,
272
- connectorEl: connectorEl,
273
- queueEl: queueEl,
274
- store: storeEl ? storeEl.lnDataStore : null,
275
- connector: connectorEl ? (connectorEl.lnApiConnector || connectorEl.lnCouchDbConnector || connectorEl.lnWebsocketConnector) : null,
276
- queue: queueEl ? queueEl.lnApiQueue : null
277
- };
278
- };
279
-
280
- // ─── Form Write Intake (native submit, claimed via preventDefault) ──
281
-
282
- _component.prototype._handleSubmitRecord = function (detail) {
283
- const children = this.findChildren();
284
- if (!children.storeEl && !children.connectorEl) {
285
- console.warn('[ln-data-coordinator] form submit claimed but neither [data-ln-data-store] nor a connector child found in "' + (this._name || '') + '"');
286
- return;
287
- }
288
-
289
- const raw = detail.data || {};
290
- const id = raw.id;
291
- const expectedVersion = raw.expected_version;
292
- const data = Object.assign({}, raw);
293
- delete data.id;
294
- delete data.expected_version;
295
-
296
- const method = detail.method.toUpperCase();
297
-
298
- if (method === 'POST') {
299
- this._fanOutCreate(children, data, detail.action);
300
- } else if (method === 'PUT' || method === 'PATCH') {
301
- this._fanOutUpdate(children, id, data, expectedVersion, detail.action);
302
- }
303
- };
304
-
305
- // ─── Parallel Fan-Out (local store write + remote connector/queue) ──────
306
-
307
- _component.prototype._fanOutCreate = function (children, data, action) {
308
- this.refreshMapper();
309
- const tempId = '_temp_' + _uuid();
310
-
311
- if (children.storeEl) {
312
- dispatch(children.storeEl, 'ln-data-store:request-create', { tempId: tempId, data: data });
313
- }
314
-
315
- if (children.queue) {
316
- dispatch(children.queueEl, 'ln-api-queue:request-enqueue', {
317
- chainKey: tempId, op: 'create', targetId: null,
318
- payload: this.mapper.egress(data), expectedVersion: null,
319
- meta: { tempId: tempId, action: action }
320
- });
321
- } else if (children.connector) {
322
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-create', {
323
- data: this.mapper.egress(data), url: action,
324
- meta: { entryId: _uuid(), queued: false, op: 'create', tempId: tempId }
325
- });
326
- }
327
- };
328
-
329
- _component.prototype._fanOutUpdate = function (children, id, data, expectedVersion, action) {
330
- this.refreshMapper();
331
-
332
- if (children.storeEl) {
333
- dispatch(children.storeEl, 'ln-data-store:request-update', { id: id, data: data });
334
- }
335
-
336
- if (children.queue) {
337
- dispatch(children.queueEl, 'ln-api-queue:request-enqueue', {
338
- chainKey: id, op: 'update', targetId: id,
339
- payload: this.mapper.egress(data), expectedVersion: expectedVersion,
340
- meta: { id: id, action: action }
341
- });
342
- } else if (children.connector) {
343
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-update', {
344
- id: id, data: this.mapper.egress(data), expected_version: expectedVersion, url: action,
345
- meta: { entryId: _uuid(), queued: false, op: 'update', id: id }
346
- });
347
- }
348
- };
349
-
350
- _component.prototype._fanOutDelete = function (children, id) {
351
- this.refreshMapper();
352
-
353
- if (children.storeEl) {
354
- dispatch(children.storeEl, 'ln-data-store:request-delete', { id: id });
355
- }
356
-
357
- if (children.queue) {
358
- dispatch(children.queueEl, 'ln-api-queue:request-enqueue', {
359
- chainKey: id, op: 'delete', targetId: id, payload: null, expectedVersion: null, meta: { id: id }
360
- });
361
- } else if (children.connector) {
362
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-delete', {
363
- id: id, meta: { entryId: _uuid(), queued: false, op: 'delete', id: id }
364
- });
365
- }
366
- };
367
-
368
- _component.prototype._fanOutBulkDelete = function (children, ids) {
369
- this.refreshMapper();
370
- const bulkKey = ids.join(',');
371
-
372
- if (children.storeEl) {
373
- dispatch(children.storeEl, 'ln-data-store:request-bulk-delete', { ids: ids });
374
- }
375
-
376
- if (children.queue) {
377
- dispatch(children.queueEl, 'ln-api-queue:request-enqueue', {
378
- chainKey: bulkKey, op: 'bulk-delete', targetId: null, payload: { ids: ids }, expectedVersion: null, meta: { bulkKey: bulkKey, ids: ids }
379
- });
380
- } else if (children.connector) {
381
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-bulk-delete', {
382
- ids: ids, meta: { entryId: _uuid(), queued: false, op: 'bulk-delete', bulkKey: bulkKey }
383
- });
384
- }
385
- };
386
-
387
- // ─── Toast Helpers ────────────────────────────────────────
388
-
389
- _component.prototype._toastFromMessage = function (message) {
390
- if (!message) return;
391
- dispatch(window, 'ln-toast:enqueue', {
392
- type: message.type || 'success',
393
- title: message.title || '',
394
- message: message.body || ''
395
- });
396
- };
397
-
398
- _component.prototype._toastFromDict = function (key) {
399
- const text = this._dict[key];
400
- if (!text) return;
401
- dispatch(window, 'ln-toast:enqueue', { type: 'error', title: '', message: text });
402
- };
403
-
404
- _component.prototype._requestStoreMutation = function (children, action, detail) {
405
- const storeEl = children.storeEl;
406
- if (!storeEl) return Promise.reject(new Error('Store element not found'));
407
-
408
- const requestId = _uuid();
409
- const receipt = this._mutationReceipts.wait(requestId);
410
- dispatch(storeEl, 'ln-data-store:request-' + action, Object.assign({}, detail, { requestId }));
411
- return receipt;
412
- };
413
-
414
- _component.prototype._reportReconciliationError = function (operation, error, meta) {
415
- if (this._destroyed) return;
416
- dispatch(this.dom, 'ln-data-coordinator:error', {
417
- operation,
418
- error,
419
- meta: meta || null
420
- });
421
- };
422
-
423
- // ─── Event Binding ────────────────────────────────────────
424
-
425
- function _bindEvents(self) {
426
- self._handlers = {
427
- sync: function (e) {
428
- self.refreshMapper();
429
- const children = self.findChildren();
430
- if (!children.store || !children.connector) {
431
- console.warn('[ln-data-coordinator] Cannot sync: store or connector not found in subtree');
432
- return;
433
- }
434
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-sync', { since: e.detail.since, meta: { op: 'sync' } });
435
- },
436
-
437
- requestPage: function (e) {
438
- const children = self.findChildren();
439
- if (!children.connectorEl) return;
440
- const detail = e.detail || {};
441
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-query', {
442
- query: Object.assign({}, detail.query, {
443
- offset: detail.offset,
444
- limit: detail.limit,
445
- queryGen: detail.queryGen
446
- })
447
- });
448
- },
449
-
450
- reqCreate: function (e) {
451
- const children = self.findChildren();
452
- self._fanOutCreate(children, e.detail.data || {}, e.detail.action);
453
- },
454
-
455
- reqUpdate: function (e) {
456
- const children = self.findChildren();
457
- self._fanOutUpdate(children, e.detail.id, e.detail.data || {}, e.detail.expected_version, e.detail.action);
458
- },
459
-
460
- reqDelete: function (e) {
461
- const children = self.findChildren();
462
- self._fanOutDelete(children, e.detail.id);
463
- },
464
-
465
- reqBulkDelete: function (e) {
466
- const children = self.findChildren();
467
- self._fanOutBulkDelete(children, e.detail.ids || []);
468
- },
469
-
470
- queueFailed: function () {
471
- self._toastFromDict('network');
472
- },
473
-
474
- // ─── Queue Transport Executor ─────────────────────────
475
- queueSend: function (e) {
476
- self.refreshMapper();
477
- const children = self.findChildren();
478
- if (!children.store || !children.connector || !children.queue) return;
479
-
480
- const detail = e.detail || {};
481
- const entryId = detail.entryId;
482
- const op = detail.op;
483
- const targetId = detail.targetId;
484
- const payload = detail.payload;
485
- const expectedVersion = detail.expectedVersion;
486
- const queueMeta = detail.meta || {};
487
- const resourceUrl = queueMeta.action || null;
488
- const idempotencyKey = detail.idempotencyKey || entryId;
489
-
490
- if (op === 'create') {
491
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-create', {
492
- data: payload, url: resourceUrl, idempotencyKey: idempotencyKey,
493
- meta: { entryId: entryId, queued: true, op: 'create', tempId: queueMeta.tempId }
494
- });
495
- } else if (op === 'update') {
496
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-update', {
497
- id: targetId, data: payload, expected_version: expectedVersion, url: resourceUrl, idempotencyKey: idempotencyKey,
498
- meta: { entryId: entryId, queued: true, op: 'update', id: targetId }
499
- });
500
- } else if (op === 'delete') {
501
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-delete', {
502
- id: targetId, idempotencyKey: idempotencyKey,
503
- meta: { entryId: entryId, queued: true, op: 'delete', id: targetId }
504
- });
505
- } else if (op === 'bulk-delete') {
506
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-bulk-delete', {
507
- ids: (payload && payload.ids) ? payload.ids : [],
508
- idempotencyKey: idempotencyKey,
509
- meta: { entryId: entryId, queued: true, op: 'bulk-delete', bulkKey: queueMeta.bulkKey }
510
- });
511
- } else {
512
- console.warn('[ln-data-coordinator] Unknown queue op:', op);
513
- }
514
- },
515
-
516
- // ─── Form Write Intake — native submit, bubble phase ──────
517
- formSubmit: function (e) {
518
- const form = e.target;
519
- if (e.defaultPrevented) return; // ln-validate's submit gate blocked it, or another coordinator already claimed it
520
-
521
- const scopeAttr = form.hasAttribute(SCOPE_ATTR) ? form.getAttribute(SCOPE_ATTR) : null;
522
- if (scopeAttr === null) return; // form never opted in — leave native submit alone
523
-
524
- let isMine;
525
- if (scopeAttr) {
526
- isMine = self._owns(scopeAttr);
527
- } else {
528
- isMine = (form.closest('[data-ln-data-coordinator]') === self.dom);
529
- }
530
- if (!isMine) return;
531
-
532
- const method = resolveFormMethod(form);
533
- if (method !== 'POST' && method !== 'PUT' && method !== 'PATCH') return;
534
-
535
- e.preventDefault(); // claim
536
-
537
- const raw = serializeForm(form);
538
- delete raw._method;
539
- delete raw._token;
540
-
541
- self._handleSubmitRecord({ data: raw, method: method, action: form.getAttribute('action') || '' });
542
- },
543
-
544
- // ─── Connector Response Handlers (direct + queued paths) ──
545
- connFetched: function (e) {
546
- if (!e.detail) return;
547
- const meta = e.detail.meta || {};
548
- const children = self.findChildren();
549
-
550
- self.refreshMapper();
551
- const rawResponse = e.detail.data;
552
- let fetchedRecords = [], deletedIds = [], syncedAt = null;
553
-
554
- if (Array.isArray(rawResponse)) {
555
- fetchedRecords = rawResponse;
556
- syncedAt = Math.floor(Date.now() / 1000);
557
- } else if (rawResponse) {
558
- fetchedRecords = Array.isArray(rawResponse.data) ? rawResponse.data : [];
559
- deletedIds = Array.isArray(rawResponse.deleted) ? rawResponse.deleted : [];
560
- syncedAt = rawResponse.synced_at !== undefined ? rawResponse.synced_at : (rawResponse.since !== undefined ? rawResponse.since : null);
561
- }
562
-
563
- const normalizedData = fetchedRecords.map(r => self.mapper.ingress(r));
564
-
565
- // 1. Pass fetched remote data strictly to ln-data-store (Single Source of Truth)
566
- if (children.store && !children.store.initializationError) {
567
- if (meta.kind) {
568
- if (meta.kind === 'table' || meta.kind === 'list' || meta.kind === 'chart') {
569
- children.store.applyQuery(normalizedData, { total: e.detail.total }).then(function (decorated) {
570
- if (meta.queryGen != null && !self._isCurrentGen(meta.targetEl, meta.queryGen)) return;
571
- dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-loading', { loading: false });
572
- dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-data', {
573
- data: decorated,
574
- total: e.detail.total !== undefined ? e.detail.total : decorated.length,
575
- filtered: e.detail.filtered !== undefined ? e.detail.filtered : decorated.length,
576
- offset: e.detail.offset,
577
- queryGen: e.detail.queryGen
578
- });
579
- self._boundDelivered.set(meta.targetEl, true);
580
- });
581
- } else if (meta.kind === 'options') {
582
- children.store.applyQuery(normalizedData, { total: e.detail.total }).then(function () {
583
- return children.store.getAll({});
584
- }).then(function (r) {
585
- if (meta.queryGen != null && !self._isCurrentGen(meta.targetEl, meta.queryGen)) return;
586
- dispatch(meta.targetEl, 'ln-options:set-data', { data: r.data });
587
- });
588
- } else if (meta.kind === 'stat') {
589
- children.store.applyQuery(normalizedData, { total: e.detail.total }).then(function () {
590
- if (meta.queryGen != null && !self._isCurrentGen(meta.targetEl, meta.queryGen)) return;
591
- const count = e.detail.filtered !== undefined
592
- ? e.detail.filtered
593
- : (e.detail.total !== undefined ? e.detail.total : normalizedData.length);
594
- dispatch(meta.targetEl, 'ln-stat:set-count', { count: count });
595
- });
596
- }
597
- } else {
598
- children.store.applySync(normalizedData, deletedIds, syncedAt || Math.floor(Date.now() / 1000), {
599
- total: e.detail.total,
600
- filtered: e.detail.filtered,
601
- offset: e.detail.offset,
602
- queryGen: e.detail.queryGen,
603
- targetEl: meta.targetEl
604
- });
605
- }
606
- } else if (meta.targetEl && meta.kind) {
607
- if (meta.kind === 'table' || meta.kind === 'list' || meta.kind === 'chart') {
608
- dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-loading', { loading: false });
609
- dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-data', {
610
- data: normalizedData,
611
- total: e.detail.total !== undefined ? e.detail.total : normalizedData.length,
612
- filtered: e.detail.filtered !== undefined ? e.detail.filtered : normalizedData.length,
613
- offset: e.detail.offset,
614
- queryGen: e.detail.queryGen
615
- });
616
- self._boundDelivered.set(meta.targetEl, true);
617
- } else if (meta.kind === 'options') {
618
- dispatch(meta.targetEl, 'ln-options:set-data', { data: normalizedData });
619
- } else if (meta.kind === 'stat') {
620
- const count = e.detail.filtered !== undefined
621
- ? e.detail.filtered
622
- : (e.detail.total !== undefined ? e.detail.total : normalizedData.length);
623
- dispatch(meta.targetEl, 'ln-stat:set-count', { count: count });
624
- }
625
- }
626
- },
627
-
628
- connCreated: function (e) {
629
- if (!e.detail) return;
630
- const children = self.findChildren();
631
- const meta = e.detail.meta || {};
632
- if (!e.detail.record) {
633
- self._reportReconciliationError('create-empty-response', new Error('Create response missing record payload'), meta);
634
- return;
635
- }
636
- const serverRecord = self.mapper.ingress(e.detail.record);
637
- const reconciled = children.storeEl
638
- ? self._requestStoreMutation(children, 'update', { id: meta.tempId, data: serverRecord })
639
- : Promise.resolve();
640
-
641
- reconciled
642
- .then(function () {
643
- self._toastFromMessage(e.detail.message);
644
- if (meta.queued && children.queue) {
645
- dispatch(children.queueEl, 'ln-api-queue:resolve-create', {
646
- entryId: meta.entryId,
647
- oldKey: meta.tempId,
648
- newId: serverRecord.id
649
- });
650
- }
651
- })
652
- .catch(function (error) {
653
- self._reportReconciliationError('create-reconcile', error, meta);
654
- });
655
- },
656
-
657
- connUpdated: function (e) {
658
- if (!e.detail) return;
659
- const children = self.findChildren();
660
- const meta = e.detail.meta || {};
661
- const serverRecord = e.detail.record ? self.mapper.ingress(e.detail.record) : null;
662
- const reconciled = (children.storeEl && serverRecord)
663
- ? self._requestStoreMutation(children, 'update', { id: meta.id, data: serverRecord })
664
- : Promise.resolve();
665
-
666
- reconciled
667
- .then(function () {
668
- self._toastFromMessage(e.detail.message);
669
- if (meta.queued && children.queue) {
670
- dispatch(children.queueEl, 'ln-api-queue:ack', { entryId: meta.entryId });
671
- }
672
- })
673
- .catch(function (error) {
674
- self._reportReconciliationError('update-reconcile', error, meta);
675
- });
676
- },
677
-
678
- connDeleted: function (e) {
679
- const detail = e.detail || {};
680
- const children = self.findChildren();
681
- const meta = detail.meta || {};
682
- // Optimistic delete already applied; no local reconciliation.
683
- self._toastFromMessage(detail.message); // null on 204 → silent
684
- if (meta.queued && children.queue) {
685
- dispatch(children.queueEl, 'ln-api-queue:ack', { entryId: meta.entryId });
686
- }
687
- },
688
-
689
- connBulkDeleted: function (e) {
690
- const detail = e.detail || {};
691
- const children = self.findChildren();
692
- const meta = detail.meta || {};
693
- self._toastFromMessage(detail.message);
694
- if (meta.queued && children.queue) {
695
- dispatch(children.queueEl, 'ln-api-queue:ack', { entryId: meta.entryId });
696
- }
697
- },
698
-
699
- connError: function (e) {
700
- const detail = e.detail || {};
701
- const meta = detail.meta || {};
702
- const op = meta.op || detail.action;
703
- const status = detail.status || (detail.error && detail.error.status) || 0;
704
- const children = self.findChildren();
705
-
706
- if (op === 'sync') {
707
- if (children.storeEl) {
708
- dispatch(children.storeEl, 'ln-data-store:request-sync-failed', {
709
- error: detail.error,
710
- status: status
711
- });
712
- }
713
- console.error('[ln-data-coordinator] Sync failed:', detail.error);
714
- return;
715
- }
716
-
717
- if (op === 'query') {
718
- if (meta.targetEl && meta.kind) {
719
- dispatch(meta.targetEl, 'ln-' + meta.kind + ':set-loading', { loading: false });
720
- if (meta.kind === 'table' || meta.kind === 'list') {
721
- dispatch(meta.targetEl, 'ln-' + meta.kind + ':page-failed', { offset: meta.offset });
722
- }
723
- }
724
- self._reportReconciliationError('query', detail.error || detail, meta);
725
- return;
726
- }
727
-
728
- const isAuth = status === 401 || status === 419;
729
- const isTransient = status === 0 || status >= 500;
730
- const isConflict = status === 409 || status === 412;
731
-
732
- // ── Auth: pause queue, keep local write ──
733
- if (isAuth) {
734
- self._toastFromDict('auth');
735
- if (meta.queued && children.queue) {
736
- dispatch(children.queueEl, 'ln-api-queue:nack', { entryId: meta.entryId, reason: 'auth' });
737
- }
738
- return;
739
- }
740
-
741
- // ── Transient (5xx / network / 0): NEVER delete local ──
742
- if (isTransient) {
743
- if (meta.queued && children.queue) {
744
- // Retry via queue ladder; toast deferred to ln-api-queue:failed
745
- dispatch(children.queueEl, 'ln-api-queue:nack', { entryId: meta.entryId, reason: 'retry' });
746
- } else {
747
- // No queue: single attempt spent; record stays local, surface now
748
- self._toastFromDict('network');
749
- }
750
- return;
751
- }
752
-
753
- // ── Deterministic (4xx / 3xx): never retry ──
754
- let reconciliation = Promise.resolve();
755
- if (isConflict && op === 'update') {
756
- const remote = detail.data && detail.data.remote ? self.mapper.ingress(detail.data.remote) : null;
757
- if (remote && children.storeEl) {
758
- reconciliation = self._requestStoreMutation(children, 'update', { id: meta.id, data: remote });
759
- }
760
- self._toastFromDict('conflict');
761
- } else if (op === 'create') {
762
- if (children.storeEl) {
763
- reconciliation = self._requestStoreMutation(children, 'delete', { id: meta.tempId });
764
- }
765
- self._toastFromDict('rejected');
766
- } else {
767
- // update/delete/bulk generic 4xx (incl. 404): leave local, next sync reconciles
768
- self._toastFromDict('rejected');
769
- }
770
-
771
- if (meta.queued && children.queue) {
772
- reconciliation.then(function () {
773
- dispatch(children.queueEl, 'ln-api-queue:nack', { entryId: meta.entryId, reason: 'drop' });
774
- }).catch(function (error) {
775
- self._reportReconciliationError('deterministic-reconcile', error, meta);
776
- });
777
- } else {
778
- reconciliation.catch(function (error) {
779
- self._reportReconciliationError('deterministic-reconcile', error, meta);
780
- });
781
- }
782
- },
783
-
784
- // ─── Store Initialized (Sync Ownership) ───────────────
785
- storeInitialized: function (e) {
786
- const children = self.findChildren();
787
- const store = children.store;
788
- if (!store || store.initializationError || !children.connector || self._noAutosync || store.isSyncing) return;
789
-
790
- const detail = e.detail || {};
791
- if (!detail.hasCache) {
792
- store.forceSync();
793
- } else if (self._isStale()) {
794
- store.forceSync();
795
- }
796
- },
797
-
798
- // A (re)opened socket may have missed pushes — catch up with a delta
799
- // sync over whichever connector takes requests.
800
- socketConnected: function () {
801
- const children = self.findChildren();
802
- const store = children.store;
803
- if (!store || store.initializationError || !children.connector || self._noAutosync || !store.isInitialized || store.isSyncing) return;
804
- store.forceSync();
805
- },
806
-
807
- // ─── View Binder Handlers ─────────────────────────────
808
- reqTableData: function (e) { self._serveData(e, 'table'); },
809
- reqListData: function (e) { self._serveData(e, 'list'); },
810
- reqChartData: function (e) { self._serveData(e, 'chart'); },
811
- reqOptions: function (e) { self._serveOptions(e); },
812
- reqStat: function (e) { self._serveStat(e); },
813
- refreshQuery: function () { self._refreshAll(null, true); },
814
- refresh: function (e) {
815
- self._mutationReceipts.resolve(e.detail);
816
- self._refreshAll(null, false);
817
- },
818
- mutationError: function (e) {
819
- self._mutationReceipts.reject(e.detail);
820
- },
821
- refreshSynced: function (e) {
822
- if (e.detail && e.detail.changed) self._refreshAll(e.detail.meta, false);
823
- },
824
-
825
- searchChange: function (e) {
826
- e.preventDefault();
827
- const term = (e.detail && e.detail.term != null) ? e.detail.term : '';
828
- if (term === (self.dom.getAttribute(SEARCH_ATTR) || '')) return;
829
- self.dom.setAttribute(SEARCH_ATTR, term);
830
- },
831
-
832
- filterChange: function (e) {
833
- e.preventDefault();
834
- const key = e.detail && e.detail.key;
835
- if (!key) return;
836
- const values = (e.detail.values || []).slice();
837
- const filters = self._currentQuery().filters;
838
- const prev = filters[key];
839
- const unchanged = prev
840
- ? (prev.length === values.length && prev.every((v, i) => v === values[i]))
841
- : !values.length;
842
- if (unchanged) return;
843
- if (values.length) filters[key] = values;
844
- else delete filters[key];
845
- const params = new URLSearchParams();
846
- Object.keys(filters).forEach(function (k) {
847
- filters[k].forEach(function (v) { params.append(k, v); });
848
- });
849
- const next = params.toString();
850
- if (next) self.dom.setAttribute(FILTERS_ATTR, next);
851
- else self.dom.removeAttribute(FILTERS_ATTR);
852
- },
853
-
854
- sortChange: function (e) {
855
- e.preventDefault();
856
- const field = e.detail && e.detail.field;
857
- const direction = e.detail && e.detail.direction;
858
- const next = (field && direction && direction !== 'none') ? { field: field, direction: direction } : null;
859
- const prev = self._currentQuery().sort;
860
- const unchanged = (!prev && !next) || (prev && next && prev.field === next.field && prev.direction === next.direction);
861
- if (unchanged) return;
862
- if (next) {
863
- self.dom.setAttribute(SORT_FIELD_ATTR, next.field);
864
- self.dom.setAttribute(SORT_DIR_ATTR, next.direction);
865
- } else {
866
- self.dom.removeAttribute(SORT_FIELD_ATTR);
867
- self.dom.removeAttribute(SORT_DIR_ATTR);
868
- }
869
- }
870
- };
871
-
872
- // Sync request bubbling up from the child store
873
- self.dom.addEventListener('ln-data-store:request-remote-sync', self._handlers.sync);
874
- self.dom.addEventListener('ln-data-store:request-page', self._handlers.requestPage);
875
-
876
- // Coordinator-namespaced intake events (parallel fan-out)
877
- self.dom.addEventListener('ln-data-coordinator:request-create', self._handlers.reqCreate);
878
- self.dom.addEventListener('ln-data-coordinator:request-update', self._handlers.reqUpdate);
879
- self.dom.addEventListener('ln-data-coordinator:request-delete', self._handlers.reqDelete);
880
- self.dom.addEventListener('ln-data-coordinator:request-bulk-delete', self._handlers.reqBulkDelete);
881
-
882
- // Queue transport executor + terminal failure
883
- self.dom.addEventListener('ln-api-queue:send', self._handlers.queueSend);
884
- self.dom.addEventListener('ln-api-queue:failed', self._handlers.queueFailed);
885
-
886
- // Sync ownership — store initialization
887
- self.dom.addEventListener('ln-data-store:initialized', self._handlers.storeInitialized);
888
- self.dom.addEventListener('ln-websocket-connector:connected', self._handlers.socketConnected);
889
-
890
- // Form write intake — native submit, document-level, bubble phase (never
891
- // capture: ln-validate's own submit gate on the form must run first)
892
- document.addEventListener('submit', self._handlers.formSubmit);
893
-
894
- // Connector responses — generalized across concrete connector implementations
895
- CONNECTOR_RESPONSE_NAMESPACES.forEach(function (ns) {
896
- self.dom.addEventListener(ns + ':fetched', self._handlers.connFetched);
897
- self.dom.addEventListener(ns + ':created', self._handlers.connCreated);
898
- self.dom.addEventListener(ns + ':updated', self._handlers.connUpdated);
899
- self.dom.addEventListener(ns + ':deleted', self._handlers.connDeleted);
900
- self.dom.addEventListener(ns + ':bulk-deleted', self._handlers.connBulkDeleted);
901
- self.dom.addEventListener(ns + ':error', self._handlers.connError);
902
- });
903
-
904
- // View binder — request handlers (document-level to reach tables/lists outside this subtree)
905
- document.addEventListener('ln-table:request-data', self._handlers.reqTableData);
906
- document.addEventListener('ln-list:request-data', self._handlers.reqListData);
907
- document.addEventListener('ln-chart:request-data', self._handlers.reqChartData);
908
- document.addEventListener('ln-options:request-data', self._handlers.reqOptions);
909
- document.addEventListener('ln-stat:request-count', self._handlers.reqStat);
910
-
911
- // Store-change refresh — attach to self.dom so bubbling store events are caught
912
- self.dom.addEventListener('ln-data-store:ready', self._handlers.refresh);
913
- self.dom.addEventListener('ln-data-store:created', self._handlers.refresh);
914
- self.dom.addEventListener('ln-data-store:updated', self._handlers.refresh);
915
- self.dom.addEventListener('ln-data-store:deleted', self._handlers.refresh);
916
- self.dom.addEventListener('ln-data-store:mutation-error', self._handlers.mutationError);
917
- self.dom.addEventListener('ln-data-store:synced', self._handlers.refreshSynced);
918
- self.dom.addEventListener('ln-data-store:query-changed', self._handlers.refreshQuery);
919
-
920
- // Query state — owned by the coordinator, written from the axis events
921
- self.dom.addEventListener('ln-search:change', self._handlers.searchChange);
922
- self.dom.addEventListener('ln-filter:change', self._handlers.filterChange);
923
- self.dom.addEventListener('ln-sort:change', self._handlers.sortChange);
924
- }
925
-
926
- // ─── Store↔View Binder ───────────────────────────────────
927
-
928
- _component.prototype._owns = function (name) {
929
- return !!name && name === this._name;
930
- };
931
-
932
- // ─── Query State — owned by the coordinator, read live (§1 doctrine) ────
933
-
934
- _component.prototype._currentQuery = function () {
935
- const field = this.dom.getAttribute(SORT_FIELD_ATTR);
936
- const direction = this.dom.getAttribute(SORT_DIR_ATTR);
937
- const params = new URLSearchParams(this.dom.getAttribute(FILTERS_ATTR) || '');
938
- const filters = {};
939
- for (const key of new Set(params.keys())) filters[key] = params.getAll(key);
940
- return {
941
- search: this.dom.getAttribute(SEARCH_ATTR) || '',
942
- filters: filters,
943
- sort: (field && direction) ? { field: field, direction: direction } : null
944
- };
945
- };
946
-
947
- _component.prototype._nextQueryGen = function (el) {
948
- const gen = (this._queryGens.get(el) || 0) + 1;
949
- this._queryGens.set(el, gen);
950
- return gen;
951
- };
952
-
953
- _component.prototype._isCurrentGen = function (el, gen) {
954
- return this._queryGens.get(el) === gen;
955
- };
956
-
957
- _component.prototype._serveData = function (e, kind) {
958
- const el = e.target;
959
- const attrName = kind === 'table' ? 'data-ln-table-source'
960
- : (kind === 'list' ? 'data-ln-list-source' : 'data-ln-chart-source');
961
- const storeName = el.getAttribute(attrName);
962
- if (!storeName) return;
963
- if (!this._owns(storeName)) return;
964
-
965
- const request = e.detail || {};
966
- const query = normalizeDataQuery(request);
967
- this._boundQueries.set(el, query);
968
-
969
- const children = this.findChildren();
970
- const self = this;
971
- const store = children.store;
972
- const ready = store && store.ready ? store.ready : Promise.resolve();
973
-
974
- return ready.then(function () {
975
- if (self._destroyed) return;
976
- const source = selectDataSource(store, children.connector);
977
- const effective = composeQuery(query, self._currentQuery());
978
- if (source === 'remote') {
979
- // The source owns search/filter/sort even when it holds no rows yet —
980
- // the view only contributes the page window, so the server must be
981
- // asked with the composed query, not the view's request as it arrived.
982
- const gen = self._nextQueryGen(el);
983
- dispatch(el, 'ln-' + kind + ':set-loading', { loading: true });
984
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-query', {
985
- query: effective,
986
- meta: { targetEl: el, kind: kind, offset: effective.offset, limit: effective.limit, queryGen: gen }
987
- });
988
- return;
989
- }
990
-
991
- if (source !== 'store') {
992
- dispatch(el, 'ln-' + kind + ':set-loading', { loading: false });
993
- return;
994
- }
995
-
996
- const supersede = needsRemoteSupersede(store, children.connector, source);
997
- const gen = supersede ? self._nextQueryGen(el) : null;
998
- if (supersede) {
999
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-query', {
1000
- query: effective,
1001
- meta: { targetEl: el, kind: kind, offset: effective.offset, limit: effective.limit, queryGen: gen }
1002
- });
1003
- }
1004
-
1005
- return store.getAll(effective).then(function (r) {
1006
- if (self._destroyed || !self._boundDelivered) return;
1007
- if (supersede && !self._isCurrentGen(el, gen)) return; // a newer request has already superseded this one
1008
- const detail = {
1009
- data: r.data,
1010
- total: r.total,
1011
- filtered: r.filtered,
1012
- offset: request.offset !== undefined ? request.offset : r.offset,
1013
- queryGen: request.queryGen !== undefined ? request.queryGen : r.queryGen,
1014
- // The store answered from its own records while the server query
1015
- // is still out; the view renders it but keeps the refresh showing.
1016
- provisional: supersede || r.provisional === true
1017
- };
1018
- dispatch(el, 'ln-' + kind + ':set-data', detail);
1019
- self._boundDelivered.set(el, true);
1020
- });
1021
- }).catch(function (error) {
1022
- if (self._destroyed) return;
1023
- dispatch(el, 'ln-' + kind + ':set-loading', { loading: false });
1024
- dispatch(self.dom, 'ln-data-coordinator:error', {
1025
- operation: 'query',
1026
- kind: kind,
1027
- store: storeName,
1028
- target: el,
1029
- error: error
1030
- });
1031
- });
1032
- };
1033
-
1034
- _component.prototype._serveOptions = function (e) {
1035
- const el = e.target;
1036
- const name = el.getAttribute('data-ln-options');
1037
- if (!this._owns(name)) return;
1038
-
1039
- const children = this.findChildren();
1040
- const store = children.store;
1041
- const ready = store && store.ready ? store.ready : Promise.resolve();
1042
- const self = this;
1043
-
1044
- return ready.then(function () {
1045
- if (self._destroyed) return;
1046
- const source = selectDataSource(store, children.connector);
1047
- if (source === 'remote') {
1048
- const gen = self._nextQueryGen(el);
1049
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-query', {
1050
- query: {},
1051
- meta: { targetEl: el, kind: 'options', queryGen: gen }
1052
- });
1053
- return;
1054
- }
1055
- if (source !== 'store') return;
1056
-
1057
- const supersede = needsRemoteSupersede(store, children.connector, source);
1058
- const gen = supersede ? self._nextQueryGen(el) : null;
1059
- if (supersede) {
1060
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-query', {
1061
- query: {},
1062
- meta: { targetEl: el, kind: 'options', queryGen: gen }
1063
- });
1064
- }
1065
-
1066
- return store.getAll({}).then(function (r) {
1067
- if (self._destroyed) return;
1068
- if (supersede && !self._isCurrentGen(el, gen)) return;
1069
- dispatch(el, 'ln-options:set-data', { data: r.data });
1070
- });
1071
- }).catch(function (error) {
1072
- if (self._destroyed) return;
1073
- self._reportReconciliationError('options-query', error, { targetEl: el, kind: 'options' });
1074
- });
1075
- };
1076
-
1077
- _component.prototype._serveStat = function (e) {
1078
- const el = e.target;
1079
- const name = el.getAttribute('data-ln-stat');
1080
- if (!this._owns(name)) return;
1081
-
1082
- const filters = e.detail && e.detail.filters ? e.detail.filters : null;
1083
- const children = this.findChildren();
1084
- const store = children.store;
1085
- const ready = store && store.ready ? store.ready : Promise.resolve();
1086
- const self = this;
1087
-
1088
- return ready.then(function () {
1089
- if (self._destroyed) return;
1090
- const hasFilters = filters && Object.keys(filters).length > 0;
1091
- const requiresRemote = !!(children.connector && store && ((store.windowed && hasFilters) || store.noLocalQuery));
1092
- const source = requiresRemote ? 'remote' : selectDataSource(store, children.connector);
1093
- if (source === 'remote') {
1094
- const gen = self._nextQueryGen(el);
1095
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-query', {
1096
- query: { filters: filters },
1097
- meta: { targetEl: el, kind: 'stat', queryGen: gen }
1098
- });
1099
- return;
1100
- }
1101
- if (source !== 'store') return;
1102
-
1103
- const supersede = !requiresRemote && needsRemoteSupersede(store, children.connector, source);
1104
- const gen = supersede ? self._nextQueryGen(el) : null;
1105
- if (supersede) {
1106
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-query', {
1107
- query: { filters: filters },
1108
- meta: { targetEl: el, kind: 'stat', queryGen: gen }
1109
- });
1110
- }
1111
-
1112
- return store.count(filters).then(function (n) {
1113
- if (self._destroyed) return;
1114
- if (supersede && !self._isCurrentGen(el, gen)) return;
1115
- dispatch(el, 'ln-stat:set-count', { count: n });
1116
- });
1117
- }).catch(function (error) {
1118
- if (self._destroyed) return;
1119
- self._reportReconciliationError('stat-query', error, { targetEl: el, kind: 'stat' });
1120
- });
1121
- };
1122
-
1123
- _component.prototype._refreshAll = function (syncMeta, isQueryChange) {
1124
- const self = this;
1125
- const allBound = document.querySelectorAll('[data-ln-table-source],[data-ln-list-source],[data-ln-chart-source],[data-ln-options],[data-ln-stat]');
1126
- for (let i = 0; i < allBound.length; i++) {
1127
- const el = allBound[i];
1128
- let storeName, kind;
1129
-
1130
- if (el.hasAttribute('data-ln-table-source')) {
1131
- storeName = el.getAttribute('data-ln-table-source');
1132
- kind = 'table';
1133
- } else if (el.hasAttribute('data-ln-list-source')) {
1134
- storeName = el.getAttribute('data-ln-list-source');
1135
- kind = 'list';
1136
- } else if (el.hasAttribute('data-ln-chart-source')) {
1137
- storeName = el.getAttribute('data-ln-chart-source');
1138
- kind = 'chart';
1139
- } else if (el.hasAttribute('data-ln-options')) {
1140
- storeName = el.getAttribute('data-ln-options');
1141
- kind = 'options';
1142
- } else if (el.hasAttribute('data-ln-stat')) {
1143
- storeName = el.getAttribute('data-ln-stat');
1144
- kind = 'stat';
1145
- }
1146
-
1147
- if (!self._owns(storeName)) continue;
1148
-
1149
- const children = self.findChildren();
1150
- const store = children.store;
1151
-
1152
- if (kind === 'table' || kind === 'list') {
1153
- const windowAttr = kind === 'table' ? 'data-ln-table-window' : 'data-ln-list-window';
1154
- if (el.hasAttribute(windowAttr)) {
1155
- // A windowed view holds no rows to re-serve — it is told to restart
1156
- // its window (query change) or refresh it in place (post-mutation),
1157
- // and pulls the pages back through request-data.
1158
- dispatch(el, 'ln-' + kind + (isQueryChange ? ':request-invalidate' : ':request-revalidate'), {});
1159
- continue;
1160
- }
1161
- }
1162
- if (kind === 'table' || kind === 'list' || kind === 'chart') {
1163
- const cached = self._boundQueries.get(el) || { sort: null, filters: {}, search: '' };
1164
- const effective = composeQuery(cached, self._currentQuery());
1165
-
1166
- // Same read policy the view-initiated path applies. Without this the
1167
- // store-change refresh would query the cache even when the store has
1168
- // been told to leave queries to the server.
1169
- if (selectDataSource(store, children.connector) === 'remote') {
1170
- const gen = self._nextQueryGen(el);
1171
- dispatch(el, 'ln-' + kind + ':set-loading', { loading: true });
1172
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-query', {
1173
- query: effective,
1174
- meta: { targetEl: el, kind: kind, offset: effective.offset, limit: effective.limit, queryGen: gen }
1175
- });
1176
- continue;
1177
- }
1178
-
1179
- const supersede = needsRemoteSupersede(store, children.connector, selectDataSource(store, children.connector));
1180
- const gen = supersede ? self._nextQueryGen(el) : null;
1181
- if (supersede) {
1182
- dispatch(children.connectorEl, _connectorNamespace(children.connectorEl) + ':request-query', {
1183
- query: effective,
1184
- meta: { targetEl: el, kind: kind, offset: effective.offset, limit: effective.limit, queryGen: gen }
1185
- });
1186
- }
1187
-
1188
- (function (capturedEl, capturedKind, capturedSupersede, capturedGen) {
1189
- store.getAll(effective).then(function (r) {
1190
- if (self._destroyed || !self._boundDelivered) return;
1191
- if (capturedSupersede && !self._isCurrentGen(capturedEl, capturedGen)) return;
1192
- const detail = {
1193
- data: r.data,
1194
- total: (syncMeta && syncMeta.total !== undefined) ? syncMeta.total : r.total,
1195
- filtered: (syncMeta && syncMeta.filtered !== undefined) ? syncMeta.filtered : r.filtered,
1196
- offset: (r.offset !== undefined) ? r.offset
1197
- : ((syncMeta && syncMeta.offset !== undefined) ? syncMeta.offset : cached.offset),
1198
- queryGen: (r.queryGen !== undefined) ? r.queryGen
1199
- : ((syncMeta && syncMeta.queryGen !== undefined) ? syncMeta.queryGen : cached.queryGen)
1200
- };
1201
- dispatch(capturedEl, 'ln-' + capturedKind + ':set-loading', { loading: false });
1202
- dispatch(capturedEl, 'ln-' + capturedKind + ':set-data', detail);
1203
- self._boundDelivered.set(capturedEl, true);
1204
- }).catch(function () {});
1205
- })(el, kind, supersede, gen);
1206
- } else if (kind === 'options') {
1207
- (function (capturedEl) {
1208
- store.getAll({}).then(function (r) {
1209
- if (self._destroyed) return;
1210
- dispatch(capturedEl, 'ln-options:set-data', { data: r.data });
1211
- }).catch(function () {});
1212
- })(el);
1213
- } else if (kind === 'stat') {
1214
- const raw = el.getAttribute('data-ln-stat-filter');
1215
- let filters = null;
1216
- if (raw) {
1217
- const colonIdx = raw.indexOf(':');
1218
- if (colonIdx !== -1) {
1219
- const field = raw.slice(0, colonIdx).trim();
1220
- const val = raw.slice(colonIdx + 1).trim();
1221
- if (field) {
1222
- filters = {};
1223
- filters[field] = [val];
1224
- }
1225
- }
1226
- }
1227
- (function (capturedEl, capturedFilters) {
1228
- store.count(capturedFilters).then(function (n) {
1229
- if (self._destroyed) return;
1230
- dispatch(capturedEl, 'ln-stat:set-count', { count: n });
1231
- }).catch(function () {});
1232
- })(el, filters);
1233
- }
1234
- }
1235
- };
1236
-
1237
- // ─── Destroy and Cleanup ──────────────────────────────────
1238
-
1239
- _component.prototype.destroy = function () {
1240
- if (!this.dom[DOM_ATTRIBUTE]) return;
1241
-
1242
- this._destroyed = true;
1243
- const self = this;
1244
- if (self._handlers) {
1245
- self.dom.removeEventListener('ln-data-store:request-remote-sync', self._handlers.sync);
1246
- self.dom.removeEventListener('ln-data-store:request-page', self._handlers.requestPage);
1247
-
1248
- self.dom.removeEventListener('ln-data-coordinator:request-create', self._handlers.reqCreate);
1249
- self.dom.removeEventListener('ln-data-coordinator:request-update', self._handlers.reqUpdate);
1250
- self.dom.removeEventListener('ln-data-coordinator:request-delete', self._handlers.reqDelete);
1251
- self.dom.removeEventListener('ln-data-coordinator:request-bulk-delete', self._handlers.reqBulkDelete);
1252
-
1253
- self.dom.removeEventListener('ln-api-queue:send', self._handlers.queueSend);
1254
- self.dom.removeEventListener('ln-api-queue:failed', self._handlers.queueFailed);
1255
- self.dom.removeEventListener('ln-data-store:initialized', self._handlers.storeInitialized);
1256
- self.dom.removeEventListener('ln-websocket-connector:connected', self._handlers.socketConnected);
1257
-
1258
- document.removeEventListener('submit', self._handlers.formSubmit);
1259
-
1260
- CONNECTOR_RESPONSE_NAMESPACES.forEach(function (ns) {
1261
- self.dom.removeEventListener(ns + ':fetched', self._handlers.connFetched);
1262
- self.dom.removeEventListener(ns + ':created', self._handlers.connCreated);
1263
- self.dom.removeEventListener(ns + ':updated', self._handlers.connUpdated);
1264
- self.dom.removeEventListener(ns + ':deleted', self._handlers.connDeleted);
1265
- self.dom.removeEventListener(ns + ':bulk-deleted', self._handlers.connBulkDeleted);
1266
- self.dom.removeEventListener(ns + ':error', self._handlers.connError);
1267
- });
1268
-
1269
- // View binder — document-level listeners
1270
- document.removeEventListener('ln-table:request-data', self._handlers.reqTableData);
1271
- document.removeEventListener('ln-list:request-data', self._handlers.reqListData);
1272
- document.removeEventListener('ln-chart:request-data', self._handlers.reqChartData);
1273
- document.removeEventListener('ln-options:request-data', self._handlers.reqOptions);
1274
- document.removeEventListener('ln-stat:request-count', self._handlers.reqStat);
1275
-
1276
- // Store-change listeners
1277
- self.dom.removeEventListener('ln-data-store:ready', self._handlers.refresh);
1278
- self.dom.removeEventListener('ln-data-store:created', self._handlers.refresh);
1279
- self.dom.removeEventListener('ln-data-store:updated', self._handlers.refresh);
1280
- self.dom.removeEventListener('ln-data-store:deleted', self._handlers.refresh);
1281
- self.dom.removeEventListener('ln-data-store:mutation-error', self._handlers.mutationError);
1282
- self.dom.removeEventListener('ln-data-store:synced', self._handlers.refreshSynced);
1283
- self.dom.removeEventListener('ln-data-store:query-changed', self._handlers.refreshQuery);
1284
-
1285
- self.dom.removeEventListener('ln-search:change', self._handlers.searchChange);
1286
- self.dom.removeEventListener('ln-filter:change', self._handlers.filterChange);
1287
- self.dom.removeEventListener('ln-sort:change', self._handlers.sortChange);
1288
-
1289
- self._handlers = null;
1290
- }
1291
-
1292
- self._boundQueries = null;
1293
- self._boundDelivered = null;
1294
- self._queryGens = null;
1295
- self._queueQueryRefresh = null;
1296
- self._mutationReceipts.close(new Error('Data coordinator destroyed'));
1297
- self._mutationReceipts = null;
1298
-
1299
- _coordinators.delete(this);
1300
- _uninstallGlobalSync();
1301
-
1302
- delete this.dom[DOM_ATTRIBUTE];
1303
- };
1304
-
1305
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-data-coordinator', {
1306
- attributes: ATTRIBUTES
1307
- });
1308
- })();
1
+ import { registerComponent, dispatch, serializeForm, resolveFormMethod, createBatcher, attrSpec, defineAttrs, uuid } from '../../ln-core';
2
+ import { normalizeDataQuery, selectDataSource, composeQuery } 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 SCOPE_ATTR = 'data-ln-data-coordinator-scope';
9
+
10
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
11
+
12
+ const SEARCH_ATTR = 'data-ln-data-coordinator-search';
13
+ const FILTERS_ATTR = 'data-ln-data-coordinator-filters';
14
+ const SORT_FIELD_ATTR = 'data-ln-data-coordinator-sort-field';
15
+ const SORT_DIR_ATTR = 'data-ln-data-coordinator-sort-direction';
16
+
17
+ const ATTRIBUTES = {
18
+ 'data-ln-data-coordinator': { prop: '_name', type: 'string', description: 'Coordinator name or identifier for data routing' },
19
+ 'data-ln-data-coordinator-scope': { type: 'string', description: 'Scope name addressing the bound data store and connector' },
20
+ 'data-ln-data-coordinator-connector': { type: 'string', description: 'Selector, identifier, or namespace of the bound connector' },
21
+ 'data-ln-data-coordinator-mapper': { type: 'string', description: 'Name of the registered data mapper transform' },
22
+ 'data-ln-data-coordinator-search': { prop: '_searchAttr', type: 'string', description: 'Active search query term' },
23
+ 'data-ln-data-coordinator-filters': { prop: '_filtersAttr', type: 'string', description: 'Active encoded filter parameters' },
24
+ 'data-ln-data-coordinator-sort-field': { prop: '_sortFieldAttr', type: 'string', description: 'Sort field name' },
25
+ 'data-ln-data-coordinator-sort-direction': { prop: '_sortDirAttr', type: 'enum', values: ['asc', 'desc'], fallback: 'asc', description: 'Sort direction' },
26
+ 'data-ln-data-coordinator-stale': { type: 'marker', description: 'Flag indicating data needs re-synchronization' },
27
+ 'data-ln-data-coordinator-no-autosync': { type: 'boolean', description: 'Disables automatic synchronization upon state changes' }
28
+ };
29
+
30
+ const ATTR_SPEC = attrSpec(ATTRIBUTES);
31
+ const VIEW_TARGETS = [
32
+ ['[data-ln-table-source]', 'data-ln-table-source', 'table'],
33
+ ['[data-ln-list-source]', 'data-ln-list-source', 'list'],
34
+ ['[data-ln-chart-source]', 'data-ln-chart-source', 'chart'],
35
+ ['[data-ln-options]', 'data-ln-options', 'options'],
36
+ ['[data-ln-stat]', 'data-ln-stat', 'stat']
37
+ ];
38
+
39
+ function _findConnectorEl(dom) {
40
+ const target = dom.getAttribute('data-ln-data-coordinator-connector');
41
+ if (target) return dom.querySelector(target) || document.querySelector(target) || document.getElementById(target);
42
+ const standard = dom.querySelector('[data-ln-connector], [data-ln-api-connector], [data-ln-couchdb-connector], [data-ln-websocket-connector]');
43
+ if (standard) return standard;
44
+ for (const el of dom.querySelectorAll('*')) {
45
+ if (el.lnConnector) return el;
46
+ for (const a of el.attributes) if (a.name.startsWith('data-ln-') && a.name.endsWith('-connector')) return el;
47
+ }
48
+ return null;
49
+ }
50
+
51
+ function _connNs(el) {
52
+ if (!el) return 'ln-connector';
53
+ if (el.lnConnector?.namespace) return el.lnConnector.namespace;
54
+ const exp = el.getAttribute('data-ln-connector');
55
+ if (exp && exp !== 'true') return exp.startsWith('ln-') ? exp : ('ln-' + exp + (exp.endsWith('-connector') ? '' : '-connector'));
56
+ for (const a of el.attributes) if (a.name.startsWith('data-ln-') && a.name.endsWith('-connector')) return a.name.replace(/^data-/, '');
57
+ return 'ln-connector';
58
+ }
59
+
60
+ // ─── Component Constructor ─────────────────────────────
61
+ function _component(dom) {
62
+ const self = this;
63
+ this.dom = dom;
64
+ defineAttrs(this, dom, ATTR_SPEC);
65
+ if (!this._name) console.warn('[ln-data-coordinator] missing id — the coordinator cannot be addressed', dom);
66
+ dom[DOM_ATTRIBUTE] = this;
67
+
68
+ this._destroyed = false;
69
+ this.mapper = null;
70
+ this._unsubs = [];
71
+ this._boundQueries = new WeakMap();
72
+ this._boundDelivered = new WeakMap();
73
+ this._mutationReceipts = new MutationReceipts();
74
+
75
+ this._queueQueryRefresh = createBatcher(() => { if (!self._destroyed) self._refreshAll(null, true); });
76
+ this.refreshMapper();
77
+ _bindEvents(this);
78
+ this._checkInitialSync();
79
+ return this;
80
+ }
81
+
82
+ _component.prototype._noAutosync = function (c) {
83
+ return this.dom.hasAttribute('data-ln-data-coordinator-no-autosync') || !!(c.storeEl?.hasAttribute('data-ln-data-store-no-autosync'));
84
+ };
85
+
86
+ _component.prototype._isStale = function (c) {
87
+ const s = this.dom.getAttribute('data-ln-data-coordinator-stale') || c.storeEl?.getAttribute('data-ln-data-store-stale');
88
+ if (s === 'never' || s === '-1') return false;
89
+ return !c.store?.lastSyncedAt || ((Date.now() / 1000) - c.store.lastSyncedAt) > (parseInt(s, 10) || 300);
90
+ };
91
+
92
+ _component.prototype._checkInitialSync = function () {
93
+ const self = this, c = this.findChildren();
94
+ if (!c.store) return;
95
+ Promise.resolve(c.store.ready).then(() => {
96
+ if (self._destroyed) return;
97
+ const cur = self.findChildren();
98
+ if (cur.store && cur.connector && !self._noAutosync(cur) && !cur.store.isSyncing && (!cur.store.hasCache || self._isStale(cur))) {
99
+ cur.store.forceSync();
100
+ }
101
+ }).catch(err => self._reportError('store-initialize', err, null));
102
+ };
103
+
104
+ _component.prototype.refreshMapper = function () {
105
+ const name = this.dom.getAttribute('data-ln-data-coordinator-mapper') || this.dom.id;
106
+ const m = (name && window.lnCore?.getDataMapper?.(name)) || null;
107
+ this.mapper = { ingress: m?.ingress || (r => r), egress: m?.egress || (r => r) };
108
+ };
109
+
110
+ _component.prototype.findChildren = function () {
111
+ const s = this.dom.querySelector('[data-ln-data-store]'), c = _findConnectorEl(this.dom), q = this.dom.querySelector('[data-ln-api-queue]');
112
+ return { storeEl: s, connectorEl: c, queueEl: q, store: s?.lnDataStore || null, connector: c?.lnConnector || c?.lnApiConnector || c?.lnCouchDbConnector || c?.lnWebsocketConnector || null, queue: q?.lnApiQueue || null };
113
+ };
114
+
115
+ // ─── CRUD Intake & Fan-Out ───────────────────────────────
116
+ _component.prototype._fanOutRemote = function (c, op, qPayload, cPayload) {
117
+ this.refreshMapper();
118
+ if (c.queue) dispatch(c.queueEl, 'ln-api-queue:request-enqueue', qPayload);
119
+ else if (c.connector) dispatch(c.connectorEl, _connNs(c.connectorEl) + ':request-' + op, cPayload);
120
+ };
121
+
122
+ _component.prototype._fanOutCreate = function (c, data, action) {
123
+ const tempId = '_temp_' + uuid(), payload = this.mapper.egress(data);
124
+ if (c.storeEl) dispatch(c.storeEl, 'ln-data-store:request-create', { tempId, data: payload });
125
+ this._fanOutRemote(c, 'create', { chainKey: tempId, op: 'create', targetId: null, payload, expectedVersion: null, meta: { tempId, action } }, { data: payload, url: action, meta: { entryId: uuid(), queued: false, op: 'create', tempId } });
126
+ };
127
+
128
+ _component.prototype._fanOutUpdate = function (c, id, data, v, action) {
129
+ const payload = this.mapper.egress(data);
130
+ if (c.storeEl) dispatch(c.storeEl, 'ln-data-store:request-update', { id, data: payload });
131
+ this._fanOutRemote(c, 'update', { chainKey: id, op: 'update', targetId: id, payload, expectedVersion: v, meta: { id, action } }, { id, data: payload, expected_version: v, url: action, meta: { entryId: uuid(), queued: false, op: 'update', id } });
132
+ };
133
+
134
+ _component.prototype._fanOutDelete = function (c, id) {
135
+ if (c.storeEl) dispatch(c.storeEl, 'ln-data-store:request-delete', { id });
136
+ this._fanOutRemote(c, 'delete', { chainKey: id, op: 'delete', targetId: id, payload: null, expectedVersion: null, meta: { id } }, { id, meta: { entryId: uuid(), queued: false, op: 'delete', id } });
137
+ };
138
+
139
+ _component.prototype._fanOutBulkDelete = function (c, ids) {
140
+ const bulkKey = ids.join(',');
141
+ if (c.storeEl) dispatch(c.storeEl, 'ln-data-store:request-bulk-delete', { ids });
142
+ this._fanOutRemote(c, 'bulk-delete', { chainKey: bulkKey, op: 'bulk-delete', targetId: null, payload: { ids }, expectedVersion: null, meta: { bulkKey, ids } }, { ids, meta: { entryId: uuid(), queued: false, op: 'bulk-delete', bulkKey } });
143
+ };
144
+
145
+ _component.prototype._requestStoreMutation = function (c, action, detail) {
146
+ if (!c.storeEl) return Promise.reject(new Error('Store element not found'));
147
+ const requestId = uuid(), receipt = this._mutationReceipts.wait(requestId);
148
+ dispatch(c.storeEl, 'ln-data-store:request-' + action, Object.assign({}, detail, { requestId }));
149
+ return receipt;
150
+ };
151
+
152
+ _component.prototype._reportError = function (operation, error, meta) {
153
+ if (!this._destroyed) dispatch(this.dom, 'ln-data-coordinator:error', { operation, error, meta: meta || null });
154
+ };
155
+
156
+ // ─── Query State & View Serving ──────────────────────────
157
+ _component.prototype._owns = function (name) {
158
+ const target = this._name || this.findChildren().storeEl?.id;
159
+ return !!name && name === target;
160
+ };
161
+
162
+ _component.prototype._currentQuery = function () {
163
+ const field = this.dom.getAttribute(SORT_FIELD_ATTR), direction = this.dom.getAttribute(SORT_DIR_ATTR);
164
+ const params = new URLSearchParams(this.dom.getAttribute(FILTERS_ATTR) || ''), filters = {};
165
+ for (const key of new Set(params.keys())) filters[key] = params.getAll(key);
166
+ return { search: this.dom.getAttribute(SEARCH_ATTR) || '', filters, sort: (field && direction) ? { field, direction } : null };
167
+ };
168
+
169
+ _component.prototype._refreshAll = function (syncMeta, isQueryChange) {
170
+ for (const [sel, attr, kind] of VIEW_TARGETS) {
171
+ for (const el of this.dom.ownerDocument.querySelectorAll(sel)) {
172
+ if (!this._owns(el.getAttribute(attr))) continue;
173
+ if ((kind === 'table' || kind === 'list') && el.hasAttribute(kind === 'table' ? 'data-ln-table-window' : 'data-ln-list-window')) {
174
+ dispatch(el, 'ln-' + kind + (isQueryChange ? ':request-invalidate' : ':request-revalidate'), {});
175
+ continue;
176
+ }
177
+ this._serveElement(el, kind, null, syncMeta);
178
+ }
179
+ }
180
+ };
181
+
182
+ _component.prototype._serveElement = function (el, kind, reqQuery, syncMeta) {
183
+ const c = this.findChildren(), store = c.store;
184
+ if (!store) return;
185
+ if (reqQuery) this._boundQueries.set(el, reqQuery);
186
+ const cached = this._boundQueries.get(el) || { sort: null, filters: {}, search: '' };
187
+ const effective = composeQuery(cached, this._currentQuery());
188
+
189
+ const self = this;
190
+ return Promise.resolve(store.ready).then(() => {
191
+ if (self._destroyed) return;
192
+ const source = selectDataSource(store, c.connector);
193
+ if (kind === 'table' || kind === 'list' || kind === 'chart') {
194
+ if (source === 'remote') {
195
+ dispatch(el, 'ln-' + kind + ':set-loading', { loading: true });
196
+ return dispatch(c.connectorEl, _connNs(c.connectorEl) + ':request-query', { query: effective, meta: { targetEl: el, kind, offset: effective.offset, limit: effective.limit, queryGen: cached.queryGen } });
197
+ }
198
+ if (source !== 'store') return dispatch(el, 'ln-' + kind + ':set-loading', { loading: false });
199
+ return store.getAll(effective).then(r => {
200
+ if (self._destroyed || !self._boundDelivered) return;
201
+ dispatch(el, 'ln-' + kind + ':set-loading', { loading: false });
202
+ dispatch(el, 'ln-' + kind + ':set-data', { data: r.data, total: syncMeta?.total ?? r.total, filtered: syncMeta?.filtered ?? r.filtered, offset: r.offset ?? syncMeta?.offset ?? cached.offset, queryGen: cached.queryGen !== undefined ? cached.queryGen : (syncMeta?.queryGen ?? r.queryGen), provisional: r.provisional === true });
203
+ self._boundDelivered.set(el, true);
204
+ });
205
+ }
206
+ if (kind === 'options') {
207
+ if (source === 'remote') return dispatch(c.connectorEl, _connNs(c.connectorEl) + ':request-query', { query: {}, meta: { targetEl: el, kind } });
208
+ if (source === 'store') return store.getAll({}).then(r => !self._destroyed && dispatch(el, 'ln-options:set-data', { data: r.data }));
209
+ }
210
+ if (kind === 'stat') {
211
+ let filters = reqQuery?.filters;
212
+ if (!filters) {
213
+ const raw = el.getAttribute('data-ln-stat-filter');
214
+ if (raw?.includes(':')) { const p = raw.split(':'); filters = { [p[0].trim()]: [p.slice(1).join(':').trim()] }; }
215
+ }
216
+ const statSource = (c.connector && store && ((store.windowed && filters && Object.keys(filters).length) || store.noLocalQuery)) ? 'remote' : source;
217
+ if (statSource === 'remote') return dispatch(c.connectorEl, _connNs(c.connectorEl) + ':request-query', { query: { filters }, meta: { targetEl: el, kind } });
218
+ if (statSource === 'store') return store.count(filters).then(n => !self._destroyed && dispatch(el, 'ln-stat:set-count', { count: n }));
219
+ }
220
+ }).catch(err => {
221
+ if (self._destroyed) return;
222
+ if (kind === 'table' || kind === 'list' || kind === 'chart') dispatch(el, 'ln-' + kind + ':set-loading', { loading: false });
223
+ self._reportError(kind + '-query', err, { targetEl: el, kind });
224
+ });
225
+ };
226
+
227
+ // ─── Event Binding & Handlers ─────────────────────────────
228
+ function _bindEvents(self) {
229
+ const unsubs = self._unsubs = [];
230
+ const addEventListener = (target, type, fn) => { target.addEventListener(type, fn); unsubs.push(() => target.removeEventListener(type, fn)); };
231
+
232
+ addEventListener(self.dom, 'ln-data-store:request-remote-sync', e => {
233
+ self.refreshMapper();
234
+ const c = self.findChildren();
235
+ if (c.store && c.connector) dispatch(c.connectorEl, _connNs(c.connectorEl) + ':request-sync', { since: e.detail.since, meta: { op: 'sync' } });
236
+ });
237
+
238
+ addEventListener(self.dom, 'ln-data-store:request-page', e => {
239
+ const c = self.findChildren(), d = e.detail || {};
240
+ if (!c.connectorEl) return;
241
+ const effective = composeQuery(d.query || {}, self._currentQuery());
242
+ dispatch(c.connectorEl, _connNs(c.connectorEl) + ':request-query', {
243
+ query: Object.assign({}, effective, { offset: d.offset, limit: d.limit, queryGen: d.queryGen })
244
+ });
245
+ });
246
+
247
+ addEventListener(self.dom, 'ln-data-coordinator:request-create', e => self._fanOutCreate(self.findChildren(), e.detail.data || {}, e.detail.action));
248
+ addEventListener(self.dom, 'ln-data-coordinator:request-update', e => self._fanOutUpdate(self.findChildren(), e.detail.id, e.detail.data || {}, e.detail.expected_version, e.detail.action));
249
+ addEventListener(self.dom, 'ln-data-coordinator:request-delete', e => self._fanOutDelete(self.findChildren(), e.detail.id));
250
+ addEventListener(self.dom, 'ln-data-coordinator:request-bulk-delete', e => self._fanOutBulkDelete(self.findChildren(), e.detail.ids || []));
251
+ addEventListener(self.dom, 'ln-api-queue:send', e => self._onQueueSend(e.detail || {}));
252
+ addEventListener(self.dom, 'ln-api-queue:failed', e => self._reportError('queue-failed', e?.detail?.error || new Error('Queue failed'), e?.detail || null));
253
+
254
+ addEventListener(self.dom, 'ln-data-store:initialized', e => {
255
+ const c = self.findChildren();
256
+ if (c.store && !c.store.initializationError && c.connector && !self._noAutosync(c) && !c.store.isSyncing && (!e.detail?.hasCache || self._isStale(c))) c.store.forceSync();
257
+ });
258
+
259
+ const onConnected = () => {
260
+ const c = self.findChildren();
261
+ if (c.store && !c.store.initializationError && c.connector && !self._noAutosync(c) && c.store.isInitialized && !c.store.isSyncing) c.store.forceSync();
262
+ };
263
+ addEventListener(self.dom, 'ln-websocket-connector:connected', onConnected);
264
+
265
+ addEventListener(document, 'submit', e => self._onFormSubmit(e));
266
+ const serve = (e, kind, attr) => {
267
+ if (self._owns(e.target.getAttribute(attr))) self._serveElement(e.target, kind, kind === 'stat' ? { filters: e.detail?.filters || null } : (kind === 'options' ? null : normalizeDataQuery(e.detail || {})), null);
268
+ };
269
+ addEventListener(document, 'ln-table:request-data', e => serve(e, 'table', 'data-ln-table-source'));
270
+ addEventListener(document, 'ln-list:request-data', e => serve(e, 'list', 'data-ln-list-source'));
271
+ addEventListener(document, 'ln-chart:request-data', e => serve(e, 'chart', 'data-ln-chart-source'));
272
+ addEventListener(document, 'ln-options:request-data', e => serve(e, 'options', 'data-ln-options'));
273
+ addEventListener(document, 'ln-stat:request-count', e => serve(e, 'stat', 'data-ln-stat'));
274
+
275
+ const refresh = e => { self._mutationReceipts.resolve(e.detail); self._refreshAll(null, false); };
276
+ addEventListener(self.dom, 'ln-data-store:ready', refresh);
277
+ addEventListener(self.dom, 'ln-data-store:created', refresh);
278
+ addEventListener(self.dom, 'ln-data-store:updated', refresh);
279
+ addEventListener(self.dom, 'ln-data-store:deleted', refresh);
280
+ addEventListener(self.dom, 'ln-data-store:mutation-error', e => self._mutationReceipts.reject(e.detail));
281
+ addEventListener(self.dom, 'ln-data-store:synced', e => { if (e.detail?.changed) self._refreshAll(e.detail.meta, false); });
282
+ addEventListener(self.dom, 'ln-data-store:query-changed', () => self._refreshAll(null, true));
283
+
284
+ addEventListener(self.dom, 'ln-search:change', e => {
285
+ e.preventDefault();
286
+ const term = e.detail?.term != null ? e.detail.term : '';
287
+ if (term !== (self.dom.getAttribute(SEARCH_ATTR) || '')) self.dom.setAttribute(SEARCH_ATTR, term);
288
+ });
289
+
290
+ addEventListener(self.dom, 'ln-filter:change', e => {
291
+ e.preventDefault();
292
+ const key = e.detail?.key;
293
+ if (!key) return;
294
+ const values = (e.detail.values || []).slice(), filters = self._currentQuery().filters, prev = filters[key];
295
+ if (prev ? (prev.length === values.length && prev.every((v, i) => v === values[i])) : !values.length) return;
296
+ if (values.length) filters[key] = values; else delete filters[key];
297
+ const p = new URLSearchParams();
298
+ Object.keys(filters).forEach(k => filters[k].forEach(v => p.append(k, v)));
299
+ const next = p.toString();
300
+ if (next) self.dom.setAttribute(FILTERS_ATTR, next); else self.dom.removeAttribute(FILTERS_ATTR);
301
+ });
302
+
303
+ addEventListener(self.dom, 'ln-sort:change', e => {
304
+ e.preventDefault();
305
+ const f = e.detail?.field, d = e.detail?.direction;
306
+ const next = (f && d && d !== 'none') ? { field: f, direction: d } : null, prev = self._currentQuery().sort;
307
+ if ((!prev && !next) || (prev && next && prev.field === next.field && prev.direction === next.direction)) return;
308
+ if (next) { self.dom.setAttribute(SORT_FIELD_ATTR, next.field); self.dom.setAttribute(SORT_DIR_ATTR, next.direction); }
309
+ else { self.dom.removeAttribute(SORT_FIELD_ATTR); self.dom.removeAttribute(SORT_DIR_ATTR); }
310
+ });
311
+
312
+ const connNamespaces = new Set(['ln-api-connector', 'ln-couchdb-connector', 'ln-websocket-connector', 'ln-connector']);
313
+ const childConn = self.findChildren().connectorEl;
314
+ if (childConn) { const customNs = _connNs(childConn); connNamespaces.add(customNs); addEventListener(self.dom, customNs + ':connected', onConnected); }
315
+ connNamespaces.forEach(ns => {
316
+ addEventListener(self.dom, ns + ':fetched', e => self._reconcileServerFetch(e.detail));
317
+ addEventListener(self.dom, ns + ':created', e => self._reconcileServerMutation('create', e.detail));
318
+ addEventListener(self.dom, ns + ':updated', e => self._reconcileServerMutation('update', e.detail));
319
+ addEventListener(self.dom, ns + ':deleted', e => self._reconcileServerMutation('ack', e.detail));
320
+ addEventListener(self.dom, ns + ':bulk-deleted', e => self._reconcileServerMutation('ack', e.detail));
321
+ addEventListener(self.dom, ns + ':error', e => self._reconcileServerMutation('error', e.detail));
322
+ });
323
+ }
324
+
325
+ _component.prototype._onQueueSend = function (d) {
326
+ this.refreshMapper();
327
+ const c = this.findChildren();
328
+ if (!c.store || !c.connector || !c.queue) return;
329
+ const m = d.meta || {}, ns = _connNs(c.connectorEl), key = d.idempotencyKey || d.entryId;
330
+ const actions = {
331
+ create: () => dispatch(c.connectorEl, ns + ':request-create', { data: d.payload, url: m.action || null, idempotencyKey: key, meta: { entryId: d.entryId, queued: true, op: 'create', tempId: m.tempId } }),
332
+ update: () => dispatch(c.connectorEl, ns + ':request-update', { id: d.targetId, data: d.payload, expected_version: d.expectedVersion, url: m.action || null, idempotencyKey: key, meta: { entryId: d.entryId, queued: true, op: 'update', id: d.targetId } }),
333
+ delete: () => dispatch(c.connectorEl, ns + ':request-delete', { id: d.targetId, idempotencyKey: key, meta: { entryId: d.entryId, queued: true, op: 'delete', id: d.targetId } }),
334
+ 'bulk-delete': () => dispatch(c.connectorEl, ns + ':request-bulk-delete', { ids: d.payload?.ids || [], idempotencyKey: key, meta: { entryId: d.entryId, queued: true, op: 'bulk-delete', bulkKey: m.bulkKey } })
335
+ };
336
+ if (actions[d.op]) actions[d.op]();
337
+ };
338
+
339
+ _component.prototype._onFormSubmit = function (e) {
340
+ const form = e.target;
341
+ if (e.defaultPrevented) return;
342
+ const scope = form.hasAttribute(SCOPE_ATTR) ? form.getAttribute(SCOPE_ATTR) : null;
343
+ if (scope === null || (scope ? !this._owns(scope) : form.closest('[' + DOM_SELECTOR + ']') !== this.dom)) return;
344
+ const method = resolveFormMethod(form);
345
+ if (method !== 'POST' && method !== 'PUT' && method !== 'PATCH') return;
346
+ e.preventDefault();
347
+ const raw = serializeForm(form);
348
+ delete raw._method; delete raw._token;
349
+ const id = raw.id, v = raw.expected_version, action = form.getAttribute('action') || '', c = this.findChildren();
350
+ delete raw.id; delete raw.expected_version;
351
+ if (method === 'POST') this._fanOutCreate(c, raw, action);
352
+ else this._fanOutUpdate(c, id, raw, v, action);
353
+ };
354
+
355
+ _component.prototype._reconcileServerFetch = function (d) {
356
+ if (!d) return;
357
+ const meta = d.meta || {}, c = this.findChildren(), raw = d.data;
358
+ this.refreshMapper();
359
+ const fetched = Array.isArray(raw) ? raw : (raw?.data || []);
360
+ const deleted = Array.isArray(raw?.deleted) ? raw.deleted : [];
361
+ const syncedAt = Array.isArray(raw) ? Math.floor(Date.now() / 1000) : (raw?.synced_at ?? raw?.since ?? Math.floor(Date.now() / 1000));
362
+ const records = fetched.map(r => this.mapper.ingress(r));
363
+ if (!c.store || c.store.initializationError) return;
364
+
365
+ if (!meta.kind) return c.store.applySync(records, deleted, syncedAt, { total: d.total, filtered: d.filtered, offset: d.offset, queryGen: d.queryGen, targetEl: meta.targetEl });
366
+ c.store.applyQuery(records, { total: d.total }).then(dec => {
367
+ const k = meta.kind;
368
+ if (k === 'table' || k === 'list' || k === 'chart') {
369
+ dispatch(meta.targetEl, 'ln-' + k + ':set-loading', { loading: false });
370
+ dispatch(meta.targetEl, 'ln-' + k + ':set-data', { data: dec, total: d.total ?? dec.length, filtered: d.filtered ?? dec.length, offset: d.offset, queryGen: meta.queryGen });
371
+ this._boundDelivered.set(meta.targetEl, true);
372
+ } else if (k === 'options') {
373
+ c.store.getAll({}).then(r => dispatch(meta.targetEl, 'ln-options:set-data', { data: r.data }));
374
+ } else if (k === 'stat') {
375
+ dispatch(meta.targetEl, 'ln-stat:set-count', { count: d.filtered ?? d.total ?? records.length });
376
+ }
377
+ });
378
+ };
379
+
380
+ _component.prototype._reconcileServerMutation = function (type, d) {
381
+ if (!d) return;
382
+ const c = this.findChildren(), meta = d.meta || {};
383
+ if (type === 'create') {
384
+ if (!d.record) return this._reportError('create-empty-response', new Error('Create response missing record payload'), meta);
385
+ const rec = this.mapper.ingress(d.record);
386
+ (c.storeEl ? this._requestStoreMutation(c, 'update', { id: meta.tempId, data: rec }) : Promise.resolve()).then(() => {
387
+ if (meta.queued && c.queue) dispatch(c.queueEl, 'ln-api-queue:resolve-create', { entryId: meta.entryId, oldKey: meta.tempId, newId: rec.id });
388
+ }).catch(err => this._reportError('create-reconcile', err, meta));
389
+ } else if (type === 'update') {
390
+ const rec = d.record ? this.mapper.ingress(d.record) : null;
391
+ (c.storeEl && rec ? this._requestStoreMutation(c, 'update', { id: meta.id, data: rec }) : Promise.resolve()).then(() => {
392
+ if (meta.queued && c.queue) dispatch(c.queueEl, 'ln-api-queue:ack', { entryId: meta.entryId });
393
+ }).catch(err => this._reportError('update-reconcile', err, meta));
394
+ } else if (type === 'ack') {
395
+ if (meta.queued && c.queue) dispatch(c.queueEl, 'ln-api-queue:ack', { entryId: meta.entryId });
396
+ } else if (type === 'error') {
397
+ const status = d.status || d.error?.status || 0;
398
+ if (meta.queued && c.queue) {
399
+ dispatch(c.queueEl, 'ln-api-queue:nack', { entryId: meta.entryId, reason: (status === 401 || status === 419) ? 'auth' : ((status === 0 || status >= 500) ? 'retry' : 'drop') });
400
+ }
401
+ if (c.storeEl) {
402
+ dispatch(c.storeEl, 'ln-data-store:request-sync-failed', { error: d.error || d, status });
403
+ }
404
+ this._reportError('connector-error', d.error || d, meta);
405
+ }
406
+ };
407
+
408
+ // ─── Destroy and Cleanup ──────────────────────────────────
409
+ _component.prototype.destroy = function () {
410
+ if (!this.dom[DOM_ATTRIBUTE]) return;
411
+ this._destroyed = true;
412
+ while (this._unsubs?.length) this._unsubs.pop()();
413
+ this._unsubs = this._boundQueries = this._boundDelivered = this._queueQueryRefresh = null;
414
+ this._mutationReceipts.close(new Error('Data coordinator destroyed'));
415
+ this._mutationReceipts = null;
416
+ delete this.dom[DOM_ATTRIBUTE];
417
+ };
418
+
419
+ function _syncAttribute(el, attrName) {
420
+ const inst = el[DOM_ATTRIBUTE];
421
+ if (!inst) return;
422
+ if (attrName === 'data-ln-data-coordinator-mapper') inst.refreshMapper();
423
+ else if (attrName === SEARCH_ATTR || attrName === FILTERS_ATTR || attrName === SORT_FIELD_ATTR || attrName === SORT_DIR_ATTR) inst._queueQueryRefresh();
424
+ }
425
+
426
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-data-coordinator', {
427
+ extraAttributes: ['data-ln-data-coordinator-mapper', SEARCH_ATTR, FILTERS_ATTR, SORT_FIELD_ATTR, SORT_DIR_ATTR],
428
+ onAttributeChange: _syncAttribute
429
+ });
430
+
431
+ window[DOM_ATTRIBUTE].init = window[DOM_ATTRIBUTE];
432
+ })();