@livenetworks/ashlar 2.0.7 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +641 -641
  2. package/components/COMPONENTS.md +1116 -1116
  3. package/components/index.js +81 -81
  4. package/components/ln-accordion/ln-accordion-dev.scss +8 -8
  5. package/components/ln-accordion/ln-accordion.js +1 -1
  6. package/components/ln-accordion/ln-accordion.schema.json +30 -30
  7. package/components/ln-ajax/ln-ajax-dev.scss +8 -8
  8. package/components/ln-ajax/ln-ajax.js +1 -1
  9. package/components/ln-ajax/ln-ajax.schema.json +30 -30
  10. package/components/ln-api-connector/ln-api-connector.js +1 -1
  11. package/components/ln-api-connector/ln-api-connector.schema.json +9 -0
  12. package/components/ln-api-connector/src/ln-api-connector.js +2 -0
  13. package/components/ln-api-queue/ln-api-queue.js +1 -1
  14. package/components/ln-api-queue/ln-api-queue.schema.json +34 -34
  15. package/components/ln-api-queue/src/ln-api-queue.js +3 -13
  16. package/components/ln-autoresize/ln-autoresize-dev.scss +8 -8
  17. package/components/ln-autoresize/ln-autoresize.js +1 -1
  18. package/components/ln-autoresize/ln-autoresize.schema.json +22 -22
  19. package/components/ln-autosave/ln-autosave-dev.scss +13 -13
  20. package/components/ln-autosave/ln-autosave.js +1 -1
  21. package/components/ln-autosave/ln-autosave.schema.json +48 -48
  22. package/components/ln-autosave/src/autosave-model.js +34 -34
  23. package/components/ln-chart/ln-chart.js +1 -1
  24. package/components/ln-chart/ln-chart.schema.json +146 -146
  25. package/components/ln-chart/src/chart-model.js +154 -154
  26. package/components/ln-circular-progress/ln-circular-progress.js +1 -1
  27. package/components/ln-circular-progress/ln-circular-progress.schema.json +42 -42
  28. package/components/ln-confirm/ln-confirm-dev.scss +24 -24
  29. package/components/ln-confirm/ln-confirm.js +1 -1
  30. package/components/ln-confirm/ln-confirm.schema.json +66 -66
  31. package/components/ln-confirm/src/ln-confirm.js +178 -178
  32. package/components/ln-core/attrs.js +13 -0
  33. package/components/ln-core/crypto.js +286 -265
  34. package/components/ln-core/dom.js +115 -115
  35. package/components/ln-core/form.js +146 -136
  36. package/components/ln-core/helpers.js +33 -33
  37. package/components/ln-core/http.js +48 -48
  38. package/components/ln-core/index.js +1 -1
  39. package/components/ln-core/lifecycle.js +5 -0
  40. package/components/ln-core/ln-core.schema.json +3 -2
  41. package/components/ln-core/locale.js +55 -55
  42. package/components/ln-core/matching.js +78 -78
  43. package/components/ln-core/number.js +125 -125
  44. package/components/ln-core/template.js +206 -206
  45. package/components/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  46. package/components/ln-couchdb-connector/ln-couchdb-connector.schema.json +50 -50
  47. package/components/ln-couchdb-connector/src/ln-couchdb-connector.js +1 -0
  48. package/components/ln-data-coordinator/README.md +9 -38
  49. package/components/ln-data-coordinator/ln-data-coordinator-dev.scss +8 -8
  50. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -1
  51. package/components/ln-data-coordinator/ln-data-coordinator.schema.json +16 -16
  52. package/components/ln-data-coordinator/src/data-read-policy.js +0 -7
  53. package/components/ln-data-coordinator/src/ln-data-coordinator.js +432 -1308
  54. package/components/ln-data-store/ln-data-store-dev.scss +13 -13
  55. package/components/ln-data-store/ln-data-store.js +1 -1
  56. package/components/ln-data-store/ln-data-store.schema.json +84 -84
  57. package/components/ln-data-store/src/data-store-model.js +184 -184
  58. package/components/ln-data-store/src/ln-data-store.js +1 -1
  59. package/components/ln-date/ln-date.js +1 -1
  60. package/components/ln-date/ln-date.schema.json +92 -92
  61. package/components/ln-date/src/date-model.js +186 -186
  62. package/components/ln-debug/ln-debug.js +1 -1
  63. package/components/ln-debug/ln-debug.schema.json +28 -28
  64. package/components/ln-debug/src/console-sink.js +19 -19
  65. package/components/ln-debug/src/gate.js +95 -95
  66. package/components/ln-debug/src/generated-attributes.js +4 -2
  67. package/components/ln-dropdown/ln-dropdown-dev.scss +13 -13
  68. package/components/ln-dropdown/ln-dropdown.js +1 -1
  69. package/components/ln-dropdown/ln-dropdown.schema.json +90 -90
  70. package/components/ln-editor/ln-editor.js +1 -1
  71. package/components/ln-editor/ln-editor.schema.json +58 -58
  72. package/components/ln-external-links/ln-external-links.js +1 -1
  73. package/components/ln-external-links/ln-external-links.schema.json +18 -18
  74. package/components/ln-fill/ln-fill.js +1 -1
  75. package/components/ln-fill/ln-fill.schema.json +23 -23
  76. package/components/ln-filter/ln-filter.js +1 -1
  77. package/components/ln-filter/ln-filter.schema.json +153 -153
  78. package/components/ln-filter/src/filter-model.js +185 -185
  79. package/components/ln-form/ln-form-dev.scss +8 -8
  80. package/components/ln-form/ln-form.js +1 -1
  81. package/components/ln-form/ln-form.schema.json +6 -0
  82. package/components/ln-form/src/ln-form.js +21 -0
  83. package/components/ln-http/ln-http.js +1 -1
  84. package/components/ln-http/ln-http.schema.json +6 -6
  85. package/components/ln-icon/ln-icon.schema.json +6 -6
  86. package/components/ln-include/ln-include.js +1 -1
  87. package/components/ln-include/ln-include.schema.json +23 -23
  88. package/components/ln-key/ln-key-dev.scss +18 -18
  89. package/components/ln-key/ln-key.js +1 -1
  90. package/components/ln-key/ln-key.schema.json +56 -56
  91. package/components/ln-key/src/ln-key.js +10 -10
  92. package/components/ln-link/ln-link-dev.scss +8 -8
  93. package/components/ln-link/ln-link.js +1 -1
  94. package/components/ln-link/ln-link.schema.json +22 -22
  95. package/components/ln-list/README.md +152 -152
  96. package/components/ln-list/ln-list-dev.scss +8 -8
  97. package/components/ln-list/ln-list.js +1 -1
  98. package/components/ln-list/ln-list.schema.json +166 -166
  99. package/components/ln-modal/ln-modal.js +1 -1
  100. package/components/ln-modal/ln-modal.schema.json +65 -65
  101. package/components/ln-nav/ln-nav.js +1 -1
  102. package/components/ln-nav/ln-nav.schema.json +25 -25
  103. package/components/ln-number/ln-number.js +1 -1
  104. package/components/ln-number/ln-number.schema.json +63 -63
  105. package/components/ln-number/src/ln-number.js +26 -0
  106. package/components/ln-number/src/number-model.js +31 -31
  107. package/components/ln-obfuscator/README.md +60 -60
  108. package/components/ln-obfuscator/ln-obfuscator.js +1 -1
  109. package/components/ln-obfuscator/ln-obfuscator.schema.json +49 -49
  110. package/components/ln-obfuscator/src/ln-obfuscator.js +159 -159
  111. package/components/ln-obfuscator/src/obfuscator-model.js +250 -250
  112. package/components/ln-options/ln-options.js +1 -1
  113. package/components/ln-options/ln-options.schema.json +6 -0
  114. package/components/ln-options/src/ln-options.js +4 -4
  115. package/components/ln-persist/README.md +151 -151
  116. package/components/ln-persist/ln-persist.js +1 -1
  117. package/components/ln-persist/ln-persist.schema.json +28 -28
  118. package/components/ln-persist/src/ln-persist.js +83 -83
  119. package/components/ln-picklist/README.md +230 -230
  120. package/components/ln-picklist/ln-picklist-dev.scss +22 -22
  121. package/components/ln-picklist/ln-picklist.js +1 -1
  122. package/components/ln-picklist/ln-picklist.schema.json +49 -49
  123. package/components/ln-picklist/src/ln-picklist.js +228 -228
  124. package/components/ln-popover/ln-popover-dev.scss +13 -13
  125. package/components/ln-popover/ln-popover.js +1 -1
  126. package/components/ln-popover/ln-popover.schema.json +82 -82
  127. package/components/ln-progress/ln-progress-dev.scss +8 -8
  128. package/components/ln-progress/ln-progress.js +1 -1
  129. package/components/ln-progress/ln-progress.schema.json +40 -40
  130. package/components/ln-router/ln-router.js +1 -1
  131. package/components/ln-router/ln-router.schema.json +76 -76
  132. package/components/ln-scroll/README.md +45 -45
  133. package/components/ln-scroll/ln-scroll.js +1 -1
  134. package/components/ln-scroll/ln-scroll.schema.json +85 -85
  135. package/components/ln-scroll/src/ln-scroll.js +150 -150
  136. package/components/ln-search/README.md +188 -188
  137. package/components/ln-search/ln-search.js +1 -1
  138. package/components/ln-search/ln-search.schema.json +103 -103
  139. package/components/ln-slug/ln-slug.js +1 -1
  140. package/components/ln-slug/ln-slug.schema.json +23 -23
  141. package/components/ln-slug/src/ln-slug.js +25 -1
  142. package/components/ln-sort/ln-sort.js +1 -1
  143. package/components/ln-sort/ln-sort.schema.json +104 -104
  144. package/components/ln-sort/src/ln-sort.js +321 -321
  145. package/components/ln-sort/src/sort-model.js +117 -117
  146. package/components/ln-sortable/ln-sortable-dev.scss +13 -13
  147. package/components/ln-sortable/ln-sortable.js +1 -1
  148. package/components/ln-sortable/ln-sortable.schema.json +36 -36
  149. package/components/ln-stat/ln-stat-dev.scss +8 -8
  150. package/components/ln-stat/ln-stat.js +1 -1
  151. package/components/ln-stat/ln-stat.schema.json +32 -32
  152. package/components/ln-stat/src/stat-model.js +28 -28
  153. package/components/ln-table/ln-table.js +1 -1
  154. package/components/ln-table/ln-table.schema.json +211 -211
  155. package/components/ln-table/src/table-model.js +98 -98
  156. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -1
  157. package/components/ln-table-coordinator/ln-table-coordinator.schema.json +93 -93
  158. package/components/ln-tabs/ln-tabs.js +1 -1
  159. package/components/ln-tabs/ln-tabs.schema.json +75 -75
  160. package/components/ln-tabs/src/ln-tabs.js +208 -208
  161. package/components/ln-tabs/src/tabs-model.js +82 -82
  162. package/components/ln-time/ln-time-dev.scss +13 -13
  163. package/components/ln-time/ln-time.js +1 -1
  164. package/components/ln-time/ln-time.schema.json +38 -38
  165. package/components/ln-toast/ln-toast.js +1 -1
  166. package/components/ln-toast/ln-toast.schema.json +65 -65
  167. package/components/ln-toggle/ln-toggle.js +1 -1
  168. package/components/ln-toggle/ln-toggle.schema.json +52 -52
  169. package/components/ln-tooltip/ln-tooltip.js +1 -1
  170. package/components/ln-tooltip/ln-tooltip.schema.json +86 -86
  171. package/components/ln-translations/ln-translations.js +1 -1
  172. package/components/ln-translations/ln-translations.schema.json +120 -120
  173. package/components/ln-ui-coordinator/README.md +62 -62
  174. package/components/ln-ui-coordinator/ln-ui-coordinator.js +1 -1
  175. package/components/ln-ui-coordinator/ln-ui-coordinator.schema.json +28 -28
  176. package/components/ln-ui-coordinator/src/ln-ui-coordinator.js +140 -140
  177. package/components/ln-upload/ln-upload.js +1 -1
  178. package/components/ln-upload/ln-upload.schema.json +193 -193
  179. package/components/ln-upload/src/upload-model.js +78 -78
  180. package/components/ln-validate/ln-validate.js +1 -1
  181. package/components/ln-validate/ln-validate.schema.json +50 -50
  182. package/components/ln-validate/src/validate-model.js +64 -64
  183. package/components/ln-websocket-connector/README.md +166 -166
  184. package/components/ln-websocket-connector/ln-websocket-connector.js +1 -1
  185. package/components/ln-websocket-connector/ln-websocket-connector.schema.json +29 -29
  186. package/components/ln-websocket-connector/src/ln-websocket-connector.js +283 -279
  187. package/ln-ashlar.html-data.json +15 -8
  188. package/package.json +85 -84
  189. package/theme/brand.css +73 -73
  190. package/theme/components/_confirm.scss +6 -6
  191. package/theme/components/_picklist.scss +3 -3
  192. package/theme/config/_palette.scss +274 -274
  193. package/theme/config/_theme.scss +292 -292
  194. package/theme/config/_tokens.scss +285 -285
  195. package/theme/config/mixins/_index.scss +62 -62
  196. package/theme/config/mixins/_picklist.scss +66 -66
  197. package/theme/ln-ashlar-dev.scss +66 -66
  198. package/theme/ln-ashlar-landing.scss +56 -56
  199. package/theme/ln-ashlar-theme.scss +63 -63
  200. package/theme/presentation/_bento.scss +151 -151
  201. package/theme/presentation/_cta.scss +111 -111
  202. package/theme/presentation/_effects.scss +216 -216
  203. package/theme/presentation/_features.scss +108 -108
  204. package/theme/presentation/_footer.scss +129 -129
  205. package/theme/presentation/_hero.scss +158 -158
  206. package/theme/presentation/_layout.scss +170 -170
  207. package/theme/presentation/_navbar.scss +140 -140
  208. package/theme/presentation/_pricing.scss +262 -262
  209. package/theme/presentation/_stats.scss +87 -87
  210. package/theme/presentation/_testimonials.scss +124 -124
  211. package/theme/presentation/_tokens.scss +39 -39
  212. package/theme/presentation/_typography.scss +163 -163
  213. package/web-types.json +26 -12
@@ -1,279 +1,283 @@
1
- import { registerComponent, dispatch, defineAttrs, attrSpec, attrStr } from '../../ln-core';
2
-
3
- (function () {
4
- const DOM_SELECTOR = 'data-ln-websocket-connector';
5
- const DOM_ATTRIBUTE = 'lnWebsocketConnector';
6
- if (window[DOM_ATTRIBUTE] !== undefined) return;
7
-
8
- const RECONNECT_MIN_MS = 1000;
9
- const RECONNECT_MAX_MS = 30000;
10
-
11
- // ─── Attribute Contract (SSOT) ──────────────────────────
12
- // Both attributes are commands from outside. Connection status is private
13
- // state announced by events — never an attribute (DOCTRINE §3).
14
- function _applyCommand(el) {
15
- const instance = el[DOM_ATTRIBUTE];
16
- if (!instance) return;
17
- if (instance.command === 'connect') instance._open();
18
- else instance._disconnect();
19
- }
20
-
21
- function _applyUrl(el) {
22
- const instance = el[DOM_ATTRIBUTE];
23
- if (!instance || instance.command !== 'connect') return;
24
- instance._drop('url-changed');
25
- instance._open();
26
- }
27
-
28
- const ATTRIBUTES = {
29
- 'data-ln-websocket-connector': { prop: 'command', type: 'enum', values: ['connect', 'disconnect'], fallback: 'connect', effect: _applyCommand, description: 'Connection command set from outside: connect or disconnect' },
30
- 'data-ln-websocket-connector-url': { prop: 'url', read: attrStr, type: 'string', fallback: '', effect: _applyUrl, description: 'WebSocket endpoint URL (ws:// or wss://)' }
31
- };
32
- const ATTR_SPEC = attrSpec(ATTRIBUTES);
33
-
34
- // Request event → wire type. Keys are literal event names for the scanners.
35
- const REQUESTS = {
36
- 'ln-websocket-connector:request-sync': 'sync',
37
- 'ln-websocket-connector:request-query': 'query',
38
- 'ln-websocket-connector:request-create': 'create',
39
- 'ln-websocket-connector:request-update': 'update',
40
- 'ln-websocket-connector:request-delete': 'delete',
41
- 'ln-websocket-connector:request-bulk-delete': 'bulk-delete'
42
- };
43
-
44
- // Only wire fields leave the page — `meta` stays local (it carries DOM refs).
45
- function _payload(type, detail) {
46
- if (type === 'sync') return { since: detail.since };
47
- if (type === 'query') return { query: detail.query || {} };
48
- if (type === 'create') return { data: detail.data, idempotencyKey: detail.idempotencyKey };
49
- if (type === 'update') return { id: detail.id, data: detail.data, expected_version: detail.expected_version, idempotencyKey: detail.idempotencyKey };
50
- if (type === 'delete') return { id: detail.id, idempotencyKey: detail.idempotencyKey };
51
- return { ids: detail.ids, idempotencyKey: detail.idempotencyKey };
52
- }
53
-
54
- // ─── Component Constructor ─────────────────────────────
55
-
56
- function _component(dom) {
57
- this.dom = dom;
58
- defineAttrs(this, dom, ATTR_SPEC);
59
- dom[DOM_ATTRIBUTE] = this;
60
-
61
- this._socket = null;
62
- this._status = 'disconnected';
63
- this._attempt = 0;
64
- this._timer = null;
65
- this._seq = 0;
66
- this._pending = new Map(); // ref → { type, detail } awaiting a reply
67
- this._held = []; // { ref, type, detail } waiting for the socket to open
68
-
69
- const self = this;
70
- this._onRequest = function (e) {
71
- self._request(REQUESTS[e.type], e.detail || {});
72
- };
73
- for (const name in REQUESTS) dom.addEventListener(name, this._onRequest);
74
-
75
- if (this.command === 'connect') this._open();
76
- return this;
77
- }
78
-
79
- // ─── Connection ──────────────────────────────────────────
80
-
81
- _component.prototype._open = function () {
82
- if (this._socket || this._timer) return;
83
- const self = this;
84
- const url = this.url;
85
-
86
- this._attempt++;
87
- this._status = 'connecting';
88
- dispatch(this.dom, 'ln-websocket-connector:connecting', { url: url, attempt: this._attempt });
89
-
90
- let socket;
91
- try {
92
- socket = new WebSocket(url);
93
- } catch (err) {
94
- // Invalid URL or blocked mixed content — retrying cannot fix it.
95
- // A new URL (or a new connect command) opens again.
96
- this._status = 'disconnected';
97
- this._attempt = 0;
98
- this._failAll(this._held, err.message);
99
- this._held = [];
100
- dispatch(this.dom, 'ln-websocket-connector:disconnected', { url: url, code: 0, reason: err.message, willReconnect: false });
101
- return;
102
- }
103
- this._socket = socket;
104
-
105
- socket.onopen = function () {
106
- self._status = 'connected';
107
- self._attempt = 0;
108
- dispatch(self.dom, 'ln-websocket-connector:connected', { url: url });
109
- const held = self._held;
110
- self._held = [];
111
- for (let i = 0; i < held.length; i++) self._send(held[i]);
112
- };
113
-
114
- socket.onmessage = function (e) {
115
- self._receive(e.data);
116
- };
117
-
118
- socket.onclose = function (e) {
119
- self._socket = null;
120
- self._status = 'disconnected';
121
- self._failPending('connection closed');
122
- const willReconnect = self.command === 'connect';
123
- dispatch(self.dom, 'ln-websocket-connector:disconnected', { url: url, code: e.code, reason: e.reason, willReconnect: willReconnect });
124
- if (willReconnect) self._scheduleReconnect();
125
- };
126
- };
127
-
128
- _component.prototype._scheduleReconnect = function () {
129
- const self = this;
130
- const delay = Math.min(RECONNECT_MIN_MS * Math.pow(2, Math.max(0, this._attempt - 1)), RECONNECT_MAX_MS);
131
- this._timer = setTimeout(function () {
132
- self._timer = null;
133
- self._open();
134
- }, delay);
135
- };
136
-
137
- // Closes the socket (or cancels a pending reconnect) without the reconnect
138
- // path. Replies still awaited cannot arrive on a new socket, so they fail;
139
- // held requests stay held.
140
- _component.prototype._drop = function (reason) {
141
- const active = !!(this._socket || this._timer);
142
- clearTimeout(this._timer);
143
- this._timer = null;
144
- this._attempt = 0;
145
- if (this._socket) {
146
- this._socket.onopen = this._socket.onmessage = this._socket.onclose = null;
147
- this._socket.close(1000, reason);
148
- this._socket = null;
149
- }
150
- if (!active) return;
151
- this._status = 'disconnected';
152
- this._failPending('connection closed');
153
- dispatch(this.dom, 'ln-websocket-connector:disconnected', { url: this.url, code: 1000, reason: reason, willReconnect: false });
154
- };
155
-
156
- _component.prototype._disconnect = function () {
157
- this._drop('disconnect');
158
- this._failAll(this._held, 'disconnected');
159
- this._held = [];
160
- };
161
-
162
- // ─── Requests & Replies ──────────────────────────────────
163
-
164
- _component.prototype._request = function (type, detail) {
165
- const entry = { ref: String(++this._seq), type: type, detail: detail };
166
- if (this._status === 'connected') {
167
- this._send(entry);
168
- } else if (this.command === 'connect') {
169
- this._held.push(entry);
170
- } else {
171
- this._fail(entry, 0, 'disconnected', null);
172
- }
173
- };
174
-
175
- _component.prototype._send = function (entry) {
176
- this._pending.set(entry.ref, entry);
177
- this._socket.send(JSON.stringify(Object.assign({ ref: entry.ref, type: entry.type }, _payload(entry.type, entry.detail))));
178
- };
179
-
180
- _component.prototype._receive = function (raw) {
181
- let msg;
182
- try {
183
- msg = JSON.parse(raw);
184
- } catch (_) {
185
- console.warn('[ln-websocket-connector] Ignored a frame that is not JSON:', raw);
186
- return;
187
- }
188
-
189
- if (msg.ref !== undefined) {
190
- const entry = this._pending.get(String(msg.ref));
191
- if (!entry) return;
192
- this._pending.delete(entry.ref);
193
- if (msg.ok) this._resolve(entry, msg.content, msg.message || null);
194
- else this._fail(entry, msg.status || 0, msg.error, msg.data);
195
- return;
196
- }
197
-
198
- if (msg.type === 'changes') {
199
- dispatch(this.dom, 'ln-websocket-connector:fetched', {
200
- data: { data: msg.data || [], deleted: msg.deleted || [], synced_at: msg.synced_at },
201
- since: null,
202
- meta: null
203
- });
204
- }
205
- };
206
-
207
- _component.prototype._resolve = function (entry, content, message) {
208
- const d = entry.detail;
209
- const meta = d.meta || null;
210
- const dom = this.dom;
211
-
212
- if (entry.type === 'sync') {
213
- dispatch(dom, 'ln-websocket-connector:fetched', { data: content, since: d.since, meta: meta });
214
- } else if (entry.type === 'query') {
215
- const q = d.query || {};
216
- const page = content || {};
217
- dispatch(dom, 'ln-websocket-connector:fetched', {
218
- data: page.data || [], total: page.total, filtered: page.filtered,
219
- offset: q.offset, queryGen: q.queryGen, meta: meta
220
- });
221
- } else if (entry.type === 'create') {
222
- dispatch(dom, 'ln-websocket-connector:created', { record: content, tempId: d.tempId, message: message, meta: meta });
223
- } else if (entry.type === 'update') {
224
- dispatch(dom, 'ln-websocket-connector:updated', { record: content, id: d.id, message: message, meta: meta });
225
- } else if (entry.type === 'delete') {
226
- dispatch(dom, 'ln-websocket-connector:deleted', { response: content, id: d.id, message: message, meta: meta });
227
- } else {
228
- dispatch(dom, 'ln-websocket-connector:bulk-deleted', { response: content, ids: d.ids, message: message, meta: meta });
229
- }
230
- };
231
-
232
- _component.prototype._fail = function (entry, status, error, data) {
233
- const d = entry.detail;
234
- dispatch(this.dom, 'ln-websocket-connector:error', {
235
- action: entry.type,
236
- error: error,
237
- status: status,
238
- data: data || null,
239
- conflictData: status === 409 ? (data || null) : null,
240
- since: d.since, id: d.id, ids: d.ids, tempId: d.tempId,
241
- meta: d.meta || null
242
- });
243
- };
244
-
245
- _component.prototype._failAll = function (entries, error) {
246
- for (let i = 0; i < entries.length; i++) this._fail(entries[i], 0, error, null);
247
- };
248
-
249
- _component.prototype._failPending = function (error) {
250
- const entries = Array.from(this._pending.values());
251
- this._pending.clear();
252
- this._failAll(entries, error);
253
- };
254
-
255
- // ─── Teardown ────────────────────────────────────────────
256
- // Silent by doctrine: a destroyed component dispatches nothing, so held
257
- // and awaited requests are discarded, not failed.
258
-
259
- _component.prototype.destroy = function () {
260
- if (!this.dom[DOM_ATTRIBUTE]) return;
261
-
262
- for (const name in REQUESTS) this.dom.removeEventListener(name, this._onRequest);
263
- clearTimeout(this._timer);
264
- this._timer = null;
265
- if (this._socket) {
266
- this._socket.onopen = this._socket.onmessage = this._socket.onclose = null;
267
- this._socket.close(1000, 'destroy');
268
- this._socket = null;
269
- }
270
- this._pending.clear();
271
- this._held = [];
272
-
273
- delete this.dom[DOM_ATTRIBUTE];
274
- };
275
-
276
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-websocket-connector', {
277
- attributes: ATTRIBUTES
278
- });
279
- })();
1
+ import { registerComponent, dispatch, defineAttrs, attrSpec, attrStr } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-websocket-connector';
5
+ const DOM_ATTRIBUTE = 'lnWebsocketConnector';
6
+ const DOM_ALIAS = 'lnConnector';
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ const RECONNECT_MIN_MS = 1000;
10
+ const RECONNECT_MAX_MS = 30000;
11
+
12
+ // ─── Attribute Contract (SSOT) ──────────────────────────
13
+ // Both attributes are commands from outside. Connection status is private
14
+ // state announced by events — never an attribute (DOCTRINE §3).
15
+ function _applyCommand(el) {
16
+ const instance = el[DOM_ATTRIBUTE];
17
+ if (!instance) return;
18
+ if (instance.command === 'connect') instance._open();
19
+ else instance._disconnect();
20
+ }
21
+
22
+ function _applyUrl(el) {
23
+ const instance = el[DOM_ATTRIBUTE];
24
+ if (!instance || instance.command !== 'connect') return;
25
+ instance._drop('url-changed');
26
+ instance._open();
27
+ }
28
+
29
+ const ATTRIBUTES = {
30
+ 'data-ln-websocket-connector': { prop: 'command', type: 'enum', values: ['connect', 'disconnect'], fallback: 'connect', effect: _applyCommand, description: 'Connection command set from outside: connect or disconnect' },
31
+ 'data-ln-websocket-connector-url': { prop: 'url', read: attrStr, type: 'string', fallback: '', effect: _applyUrl, description: 'WebSocket endpoint URL (ws:// or wss://)' }
32
+ };
33
+ const ATTR_SPEC = attrSpec(ATTRIBUTES);
34
+
35
+ // Request event → wire type. Keys are literal event names for the scanners.
36
+ const REQUESTS = {
37
+ 'ln-websocket-connector:request-sync': 'sync',
38
+ 'ln-websocket-connector:request-query': 'query',
39
+ 'ln-websocket-connector:request-create': 'create',
40
+ 'ln-websocket-connector:request-update': 'update',
41
+ 'ln-websocket-connector:request-delete': 'delete',
42
+ 'ln-websocket-connector:request-bulk-delete': 'bulk-delete'
43
+ };
44
+
45
+ // Only wire fields leave the page — `meta` stays local (it carries DOM refs).
46
+ function _payload(type, detail) {
47
+ if (type === 'sync') return { since: detail.since };
48
+ if (type === 'query') return { query: detail.query || {} };
49
+ if (type === 'create') return { data: detail.data, idempotencyKey: detail.idempotencyKey };
50
+ if (type === 'update') return { id: detail.id, data: detail.data, expected_version: detail.expected_version, idempotencyKey: detail.idempotencyKey };
51
+ if (type === 'delete') return { id: detail.id, idempotencyKey: detail.idempotencyKey };
52
+ return { ids: detail.ids, idempotencyKey: detail.idempotencyKey };
53
+ }
54
+
55
+ // ─── Component Constructor ─────────────────────────────
56
+
57
+ function _component(dom) {
58
+ this.dom = dom;
59
+ defineAttrs(this, dom, ATTR_SPEC);
60
+ dom[DOM_ATTRIBUTE] = this;
61
+ dom[DOM_ALIAS] = this;
62
+ this.namespace = 'ln-websocket-connector';
63
+
64
+ this._socket = null;
65
+ this._status = 'disconnected';
66
+ this._attempt = 0;
67
+ this._timer = null;
68
+ this._seq = 0;
69
+ this._pending = new Map(); // ref → { type, detail } awaiting a reply
70
+ this._held = []; // { ref, type, detail } waiting for the socket to open
71
+
72
+ const self = this;
73
+ this._onRequest = function (e) {
74
+ self._request(REQUESTS[e.type], e.detail || {});
75
+ };
76
+ for (const name in REQUESTS) dom.addEventListener(name, this._onRequest);
77
+
78
+ if (this.command === 'connect') this._open();
79
+ return this;
80
+ }
81
+
82
+ // ─── Connection ──────────────────────────────────────────
83
+
84
+ _component.prototype._open = function () {
85
+ if (this._socket || this._timer) return;
86
+ const self = this;
87
+ const url = this.url;
88
+
89
+ this._attempt++;
90
+ this._status = 'connecting';
91
+ dispatch(this.dom, 'ln-websocket-connector:connecting', { url: url, attempt: this._attempt });
92
+
93
+ let socket;
94
+ try {
95
+ socket = new WebSocket(url);
96
+ } catch (err) {
97
+ // Invalid URL or blocked mixed content — retrying cannot fix it.
98
+ // A new URL (or a new connect command) opens again.
99
+ this._status = 'disconnected';
100
+ this._attempt = 0;
101
+ this._failAll(this._held, err.message);
102
+ this._held = [];
103
+ dispatch(this.dom, 'ln-websocket-connector:disconnected', { url: url, code: 0, reason: err.message, willReconnect: false });
104
+ return;
105
+ }
106
+ this._socket = socket;
107
+
108
+ socket.onopen = function () {
109
+ self._status = 'connected';
110
+ self._attempt = 0;
111
+ dispatch(self.dom, 'ln-websocket-connector:connected', { url: url });
112
+ const held = self._held;
113
+ self._held = [];
114
+ for (let i = 0; i < held.length; i++) self._send(held[i]);
115
+ };
116
+
117
+ socket.onmessage = function (e) {
118
+ self._receive(e.data);
119
+ };
120
+
121
+ socket.onclose = function (e) {
122
+ self._socket = null;
123
+ self._status = 'disconnected';
124
+ self._failPending('connection closed');
125
+ const willReconnect = self.command === 'connect';
126
+ dispatch(self.dom, 'ln-websocket-connector:disconnected', { url: url, code: e.code, reason: e.reason, willReconnect: willReconnect });
127
+ if (willReconnect) self._scheduleReconnect();
128
+ };
129
+ };
130
+
131
+ _component.prototype._scheduleReconnect = function () {
132
+ const self = this;
133
+ const delay = Math.min(RECONNECT_MIN_MS * Math.pow(2, Math.max(0, this._attempt - 1)), RECONNECT_MAX_MS);
134
+ this._timer = setTimeout(function () {
135
+ self._timer = null;
136
+ self._open();
137
+ }, delay);
138
+ };
139
+
140
+ // Closes the socket (or cancels a pending reconnect) without the reconnect
141
+ // path. Replies still awaited cannot arrive on a new socket, so they fail;
142
+ // held requests stay held.
143
+ _component.prototype._drop = function (reason) {
144
+ const active = !!(this._socket || this._timer);
145
+ clearTimeout(this._timer);
146
+ this._timer = null;
147
+ this._attempt = 0;
148
+ if (this._socket) {
149
+ this._socket.onopen = this._socket.onmessage = this._socket.onclose = null;
150
+ this._socket.close(1000, reason);
151
+ this._socket = null;
152
+ }
153
+ if (!active) return;
154
+ this._status = 'disconnected';
155
+ this._failPending('connection closed');
156
+ dispatch(this.dom, 'ln-websocket-connector:disconnected', { url: this.url, code: 1000, reason: reason, willReconnect: false });
157
+ };
158
+
159
+ _component.prototype._disconnect = function () {
160
+ this._drop('disconnect');
161
+ this._failAll(this._held, 'disconnected');
162
+ this._held = [];
163
+ };
164
+
165
+ // ─── Requests & Replies ──────────────────────────────────
166
+
167
+ _component.prototype._request = function (type, detail) {
168
+ const entry = { ref: String(++this._seq), type: type, detail: detail };
169
+ if (this._status === 'connected') {
170
+ this._send(entry);
171
+ } else if (this.command === 'connect') {
172
+ this._held.push(entry);
173
+ } else {
174
+ this._fail(entry, 0, 'disconnected', null);
175
+ }
176
+ };
177
+
178
+ _component.prototype._send = function (entry) {
179
+ this._pending.set(entry.ref, entry);
180
+ this._socket.send(JSON.stringify(Object.assign({ ref: entry.ref, type: entry.type }, _payload(entry.type, entry.detail))));
181
+ };
182
+
183
+ _component.prototype._receive = function (raw) {
184
+ let msg;
185
+ try {
186
+ msg = JSON.parse(raw);
187
+ } catch (_) {
188
+ console.warn('[ln-websocket-connector] Ignored a frame that is not JSON:', raw);
189
+ return;
190
+ }
191
+
192
+ if (msg.ref !== undefined) {
193
+ const entry = this._pending.get(String(msg.ref));
194
+ if (!entry) return;
195
+ this._pending.delete(entry.ref);
196
+ if (msg.ok) this._resolve(entry, msg.content, msg.message || null);
197
+ else this._fail(entry, msg.status || 0, msg.error, msg.data);
198
+ return;
199
+ }
200
+
201
+ if (msg.type === 'changes') {
202
+ dispatch(this.dom, 'ln-websocket-connector:fetched', {
203
+ data: { data: msg.data || [], deleted: msg.deleted || [], synced_at: msg.synced_at },
204
+ since: null,
205
+ meta: null
206
+ });
207
+ }
208
+ };
209
+
210
+ _component.prototype._resolve = function (entry, content, message) {
211
+ const d = entry.detail;
212
+ const meta = d.meta || null;
213
+ const dom = this.dom;
214
+
215
+ if (entry.type === 'sync') {
216
+ dispatch(dom, 'ln-websocket-connector:fetched', { data: content, since: d.since, meta: meta });
217
+ } else if (entry.type === 'query') {
218
+ const q = d.query || {};
219
+ const page = content || {};
220
+ dispatch(dom, 'ln-websocket-connector:fetched', {
221
+ data: page.data || [], total: page.total, filtered: page.filtered,
222
+ offset: q.offset, queryGen: q.queryGen, meta: meta
223
+ });
224
+ } else if (entry.type === 'create') {
225
+ dispatch(dom, 'ln-websocket-connector:created', { record: content, tempId: d.tempId, message: message, meta: meta });
226
+ } else if (entry.type === 'update') {
227
+ dispatch(dom, 'ln-websocket-connector:updated', { record: content, id: d.id, message: message, meta: meta });
228
+ } else if (entry.type === 'delete') {
229
+ dispatch(dom, 'ln-websocket-connector:deleted', { response: content, id: d.id, message: message, meta: meta });
230
+ } else {
231
+ dispatch(dom, 'ln-websocket-connector:bulk-deleted', { response: content, ids: d.ids, message: message, meta: meta });
232
+ }
233
+ };
234
+
235
+ _component.prototype._fail = function (entry, status, error, data) {
236
+ const d = entry.detail;
237
+ dispatch(this.dom, 'ln-websocket-connector:error', {
238
+ action: entry.type,
239
+ error: error,
240
+ status: status,
241
+ data: data || null,
242
+ conflictData: status === 409 ? (data || null) : null,
243
+ since: d.since, id: d.id, ids: d.ids, tempId: d.tempId,
244
+ meta: d.meta || null
245
+ });
246
+ };
247
+
248
+ _component.prototype._failAll = function (entries, error) {
249
+ for (let i = 0; i < entries.length; i++) this._fail(entries[i], 0, error, null);
250
+ };
251
+
252
+ _component.prototype._failPending = function (error) {
253
+ const entries = Array.from(this._pending.values());
254
+ this._pending.clear();
255
+ this._failAll(entries, error);
256
+ };
257
+
258
+ // ─── Teardown ────────────────────────────────────────────
259
+ // Silent by doctrine: a destroyed component dispatches nothing, so held
260
+ // and awaited requests are discarded, not failed.
261
+
262
+ _component.prototype.destroy = function () {
263
+ if (!this.dom[DOM_ATTRIBUTE]) return;
264
+
265
+ for (const name in REQUESTS) this.dom.removeEventListener(name, this._onRequest);
266
+ clearTimeout(this._timer);
267
+ this._timer = null;
268
+ if (this._socket) {
269
+ this._socket.onopen = this._socket.onmessage = this._socket.onclose = null;
270
+ this._socket.close(1000, 'destroy');
271
+ this._socket = null;
272
+ }
273
+ this._pending.clear();
274
+ this._held = [];
275
+
276
+ delete this.dom[DOM_ATTRIBUTE];
277
+ delete this.dom[DOM_ALIAS];
278
+ };
279
+
280
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-websocket-connector', {
281
+ attributes: ATTRIBUTES
282
+ });
283
+ })();
@@ -24,6 +24,10 @@
24
24
  "name": "data-ln-api-connector-query-debounce",
25
25
  "description": "Debounce delay in milliseconds before dispatching query requests"
26
26
  },
27
+ {
28
+ "name": "data-ln-api-endpoint",
29
+ "description": "Alias for data-ln-api-path"
30
+ },
27
31
  {
28
32
  "name": "data-ln-api-headers",
29
33
  "description": "Custom HTTP headers in JSON format or semicolon-separated pairs"
@@ -224,6 +228,10 @@
224
228
  "name": "data-ln-confirm-timeout",
225
229
  "description": "Confirmation timeout in seconds before reverting"
226
230
  },
231
+ {
232
+ "name": "data-ln-connector",
233
+ "description": "data-ln-connector attribute"
234
+ },
227
235
  {
228
236
  "name": "data-ln-couchdb-auth",
229
237
  "description": "Authentication credentials for CouchDB requests"
@@ -250,9 +258,8 @@
250
258
  "description": "Coordinator name or identifier for data routing"
251
259
  },
252
260
  {
253
- "name": "data-ln-data-coordinator-dict",
254
- "description": "Marks dictionary container for coordinator translatable messages",
255
- "valueSet": "v"
261
+ "name": "data-ln-data-coordinator-connector",
262
+ "description": "Selector, identifier, or namespace of the bound connector"
256
263
  },
257
264
  {
258
265
  "name": "data-ln-data-coordinator-filters",
@@ -289,7 +296,7 @@
289
296
  },
290
297
  {
291
298
  "name": "data-ln-data-coordinator-sort-field",
292
- "description": "Active sort field property name"
299
+ "description": "Sort field name"
293
300
  },
294
301
  {
295
302
  "name": "data-ln-data-coordinator-stale",
@@ -669,6 +676,10 @@
669
676
  "name": "data-ln-include",
670
677
  "description": "URL of external HTML template to fetch and include"
671
678
  },
679
+ {
680
+ "name": "data-ln-initial-value",
681
+ "description": "data-ln-initial-value attribute"
682
+ },
672
683
  {
673
684
  "name": "data-ln-item",
674
685
  "description": "data-ln-item attribute"
@@ -769,10 +780,6 @@
769
780
  "name": "data-ln-list-window-threshold",
770
781
  "description": "Scroll threshold margin in pixels to trigger page fetch"
771
782
  },
772
- {
773
- "name": "data-ln-mapper",
774
- "description": "data-ln-mapper attribute"
775
- },
776
783
  {
777
784
  "name": "data-ln-modal",
778
785
  "description": "Control state of the modal dialog",