@livenetworks/ashlar 2.0.6 → 2.0.8

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