@livenetworks/ashlar 2.0.7 → 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 (207) 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 +96 -96
  12. package/components/ln-api-connector/src/ln-api-connector.js +1 -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/crypto.js +286 -265
  33. package/components/ln-core/dom.js +115 -115
  34. package/components/ln-core/form.js +136 -136
  35. package/components/ln-core/helpers.js +33 -33
  36. package/components/ln-core/http.js +48 -48
  37. package/components/ln-core/index.js +1 -1
  38. package/components/ln-core/ln-core.schema.json +109 -109
  39. package/components/ln-core/locale.js +55 -55
  40. package/components/ln-core/matching.js +78 -78
  41. package/components/ln-core/number.js +125 -125
  42. package/components/ln-core/template.js +206 -206
  43. package/components/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  44. package/components/ln-couchdb-connector/ln-couchdb-connector.schema.json +50 -50
  45. package/components/ln-couchdb-connector/src/ln-couchdb-connector.js +1 -0
  46. package/components/ln-data-coordinator/README.md +9 -38
  47. package/components/ln-data-coordinator/ln-data-coordinator-dev.scss +8 -8
  48. package/components/ln-data-coordinator/ln-data-coordinator.js +1 -1
  49. package/components/ln-data-coordinator/ln-data-coordinator.schema.json +16 -16
  50. package/components/ln-data-coordinator/src/data-read-policy.js +0 -7
  51. package/components/ln-data-coordinator/src/ln-data-coordinator.js +428 -1308
  52. package/components/ln-data-store/ln-data-store-dev.scss +13 -13
  53. package/components/ln-data-store/ln-data-store.js +1 -1
  54. package/components/ln-data-store/ln-data-store.schema.json +84 -84
  55. package/components/ln-data-store/src/data-store-model.js +184 -184
  56. package/components/ln-data-store/src/ln-data-store.js +1 -1
  57. package/components/ln-date/ln-date.js +1 -1
  58. package/components/ln-date/ln-date.schema.json +92 -92
  59. package/components/ln-date/src/date-model.js +186 -186
  60. package/components/ln-debug/ln-debug.js +1 -1
  61. package/components/ln-debug/ln-debug.schema.json +28 -28
  62. package/components/ln-debug/src/console-sink.js +19 -19
  63. package/components/ln-debug/src/gate.js +95 -95
  64. package/components/ln-debug/src/generated-attributes.js +2 -2
  65. package/components/ln-dropdown/ln-dropdown-dev.scss +13 -13
  66. package/components/ln-dropdown/ln-dropdown.js +1 -1
  67. package/components/ln-dropdown/ln-dropdown.schema.json +90 -90
  68. package/components/ln-editor/ln-editor.js +1 -1
  69. package/components/ln-editor/ln-editor.schema.json +58 -58
  70. package/components/ln-external-links/ln-external-links.js +1 -1
  71. package/components/ln-external-links/ln-external-links.schema.json +18 -18
  72. package/components/ln-fill/ln-fill.js +1 -1
  73. package/components/ln-fill/ln-fill.schema.json +23 -23
  74. package/components/ln-filter/ln-filter.js +1 -1
  75. package/components/ln-filter/ln-filter.schema.json +153 -153
  76. package/components/ln-filter/src/filter-model.js +185 -185
  77. package/components/ln-form/ln-form-dev.scss +8 -8
  78. package/components/ln-form/ln-form.js +1 -1
  79. package/components/ln-form/ln-form.schema.json +45 -45
  80. package/components/ln-http/ln-http.js +1 -1
  81. package/components/ln-http/ln-http.schema.json +6 -6
  82. package/components/ln-icon/ln-icon.schema.json +6 -6
  83. package/components/ln-include/ln-include.js +1 -1
  84. package/components/ln-include/ln-include.schema.json +23 -23
  85. package/components/ln-key/ln-key-dev.scss +18 -18
  86. package/components/ln-key/ln-key.js +1 -1
  87. package/components/ln-key/ln-key.schema.json +56 -56
  88. package/components/ln-key/src/ln-key.js +10 -10
  89. package/components/ln-link/ln-link-dev.scss +8 -8
  90. package/components/ln-link/ln-link.js +1 -1
  91. package/components/ln-link/ln-link.schema.json +22 -22
  92. package/components/ln-list/README.md +152 -152
  93. package/components/ln-list/ln-list-dev.scss +8 -8
  94. package/components/ln-list/ln-list.js +1 -1
  95. package/components/ln-list/ln-list.schema.json +166 -166
  96. package/components/ln-modal/ln-modal.js +1 -1
  97. package/components/ln-modal/ln-modal.schema.json +65 -65
  98. package/components/ln-nav/ln-nav.js +1 -1
  99. package/components/ln-nav/ln-nav.schema.json +25 -25
  100. package/components/ln-number/ln-number.js +1 -1
  101. package/components/ln-number/ln-number.schema.json +63 -63
  102. package/components/ln-number/src/number-model.js +31 -31
  103. package/components/ln-obfuscator/README.md +60 -60
  104. package/components/ln-obfuscator/ln-obfuscator.js +1 -1
  105. package/components/ln-obfuscator/ln-obfuscator.schema.json +49 -49
  106. package/components/ln-obfuscator/src/ln-obfuscator.js +159 -159
  107. package/components/ln-obfuscator/src/obfuscator-model.js +250 -250
  108. package/components/ln-options/ln-options.js +1 -1
  109. package/components/ln-options/ln-options.schema.json +41 -41
  110. package/components/ln-persist/README.md +151 -151
  111. package/components/ln-persist/ln-persist.js +1 -1
  112. package/components/ln-persist/ln-persist.schema.json +28 -28
  113. package/components/ln-persist/src/ln-persist.js +83 -83
  114. package/components/ln-picklist/README.md +230 -230
  115. package/components/ln-picklist/ln-picklist-dev.scss +22 -22
  116. package/components/ln-picklist/ln-picklist.js +1 -1
  117. package/components/ln-picklist/ln-picklist.schema.json +49 -49
  118. package/components/ln-picklist/src/ln-picklist.js +228 -228
  119. package/components/ln-popover/ln-popover-dev.scss +13 -13
  120. package/components/ln-popover/ln-popover.js +1 -1
  121. package/components/ln-popover/ln-popover.schema.json +82 -82
  122. package/components/ln-progress/ln-progress-dev.scss +8 -8
  123. package/components/ln-progress/ln-progress.js +1 -1
  124. package/components/ln-progress/ln-progress.schema.json +40 -40
  125. package/components/ln-router/ln-router.js +1 -1
  126. package/components/ln-router/ln-router.schema.json +76 -76
  127. package/components/ln-scroll/README.md +45 -45
  128. package/components/ln-scroll/ln-scroll.js +1 -1
  129. package/components/ln-scroll/ln-scroll.schema.json +85 -85
  130. package/components/ln-scroll/src/ln-scroll.js +150 -150
  131. package/components/ln-search/README.md +188 -188
  132. package/components/ln-search/ln-search.js +1 -1
  133. package/components/ln-search/ln-search.schema.json +103 -103
  134. package/components/ln-slug/ln-slug.js +1 -1
  135. package/components/ln-slug/ln-slug.schema.json +23 -23
  136. package/components/ln-sort/ln-sort.js +1 -1
  137. package/components/ln-sort/ln-sort.schema.json +104 -104
  138. package/components/ln-sort/src/ln-sort.js +321 -321
  139. package/components/ln-sort/src/sort-model.js +117 -117
  140. package/components/ln-sortable/ln-sortable-dev.scss +13 -13
  141. package/components/ln-sortable/ln-sortable.js +1 -1
  142. package/components/ln-sortable/ln-sortable.schema.json +36 -36
  143. package/components/ln-stat/ln-stat-dev.scss +8 -8
  144. package/components/ln-stat/ln-stat.js +1 -1
  145. package/components/ln-stat/ln-stat.schema.json +32 -32
  146. package/components/ln-stat/src/stat-model.js +28 -28
  147. package/components/ln-table/ln-table.js +1 -1
  148. package/components/ln-table/ln-table.schema.json +211 -211
  149. package/components/ln-table/src/table-model.js +98 -98
  150. package/components/ln-table-coordinator/ln-table-coordinator.js +1 -1
  151. package/components/ln-table-coordinator/ln-table-coordinator.schema.json +93 -93
  152. package/components/ln-tabs/ln-tabs.js +1 -1
  153. package/components/ln-tabs/ln-tabs.schema.json +75 -75
  154. package/components/ln-tabs/src/ln-tabs.js +208 -208
  155. package/components/ln-tabs/src/tabs-model.js +82 -82
  156. package/components/ln-time/ln-time-dev.scss +13 -13
  157. package/components/ln-time/ln-time.js +1 -1
  158. package/components/ln-time/ln-time.schema.json +38 -38
  159. package/components/ln-toast/ln-toast.js +1 -1
  160. package/components/ln-toast/ln-toast.schema.json +65 -65
  161. package/components/ln-toggle/ln-toggle.js +1 -1
  162. package/components/ln-toggle/ln-toggle.schema.json +52 -52
  163. package/components/ln-tooltip/ln-tooltip.js +1 -1
  164. package/components/ln-tooltip/ln-tooltip.schema.json +86 -86
  165. package/components/ln-translations/ln-translations.js +1 -1
  166. package/components/ln-translations/ln-translations.schema.json +120 -120
  167. package/components/ln-ui-coordinator/README.md +62 -62
  168. package/components/ln-ui-coordinator/ln-ui-coordinator.js +1 -1
  169. package/components/ln-ui-coordinator/ln-ui-coordinator.schema.json +28 -28
  170. package/components/ln-ui-coordinator/src/ln-ui-coordinator.js +140 -140
  171. package/components/ln-upload/ln-upload.js +1 -1
  172. package/components/ln-upload/ln-upload.schema.json +193 -193
  173. package/components/ln-upload/src/upload-model.js +78 -78
  174. package/components/ln-validate/ln-validate.js +1 -1
  175. package/components/ln-validate/ln-validate.schema.json +50 -50
  176. package/components/ln-validate/src/validate-model.js +64 -64
  177. package/components/ln-websocket-connector/README.md +166 -166
  178. package/components/ln-websocket-connector/ln-websocket-connector.js +1 -1
  179. package/components/ln-websocket-connector/ln-websocket-connector.schema.json +29 -29
  180. package/components/ln-websocket-connector/src/ln-websocket-connector.js +283 -279
  181. package/ln-ashlar.html-data.json +7 -8
  182. package/package.json +84 -84
  183. package/theme/brand.css +73 -73
  184. package/theme/components/_confirm.scss +6 -6
  185. package/theme/components/_picklist.scss +3 -3
  186. package/theme/config/_palette.scss +274 -274
  187. package/theme/config/_theme.scss +292 -292
  188. package/theme/config/_tokens.scss +285 -285
  189. package/theme/config/mixins/_index.scss +62 -62
  190. package/theme/config/mixins/_picklist.scss +66 -66
  191. package/theme/ln-ashlar-dev.scss +66 -66
  192. package/theme/ln-ashlar-landing.scss +56 -56
  193. package/theme/ln-ashlar-theme.scss +63 -63
  194. package/theme/presentation/_bento.scss +151 -151
  195. package/theme/presentation/_cta.scss +111 -111
  196. package/theme/presentation/_effects.scss +216 -216
  197. package/theme/presentation/_features.scss +108 -108
  198. package/theme/presentation/_footer.scss +129 -129
  199. package/theme/presentation/_hero.scss +158 -158
  200. package/theme/presentation/_layout.scss +170 -170
  201. package/theme/presentation/_navbar.scss +140 -140
  202. package/theme/presentation/_pricing.scss +262 -262
  203. package/theme/presentation/_stats.scss +87 -87
  204. package/theme/presentation/_testimonials.scss +124 -124
  205. package/theme/presentation/_tokens.scss +39 -39
  206. package/theme/presentation/_typography.scss +163 -163
  207. package/web-types.json +12 -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
+ })();
@@ -224,6 +224,10 @@
224
224
  "name": "data-ln-confirm-timeout",
225
225
  "description": "Confirmation timeout in seconds before reverting"
226
226
  },
227
+ {
228
+ "name": "data-ln-connector",
229
+ "description": "data-ln-connector attribute"
230
+ },
227
231
  {
228
232
  "name": "data-ln-couchdb-auth",
229
233
  "description": "Authentication credentials for CouchDB requests"
@@ -250,9 +254,8 @@
250
254
  "description": "Coordinator name or identifier for data routing"
251
255
  },
252
256
  {
253
- "name": "data-ln-data-coordinator-dict",
254
- "description": "Marks dictionary container for coordinator translatable messages",
255
- "valueSet": "v"
257
+ "name": "data-ln-data-coordinator-connector",
258
+ "description": "Selector, identifier, or namespace of the bound connector"
256
259
  },
257
260
  {
258
261
  "name": "data-ln-data-coordinator-filters",
@@ -289,7 +292,7 @@
289
292
  },
290
293
  {
291
294
  "name": "data-ln-data-coordinator-sort-field",
292
- "description": "Active sort field property name"
295
+ "description": "Sort field name"
293
296
  },
294
297
  {
295
298
  "name": "data-ln-data-coordinator-stale",
@@ -769,10 +772,6 @@
769
772
  "name": "data-ln-list-window-threshold",
770
773
  "description": "Scroll threshold margin in pixels to trigger page fetch"
771
774
  },
772
- {
773
- "name": "data-ln-mapper",
774
- "description": "data-ln-mapper attribute"
775
- },
776
775
  {
777
776
  "name": "data-ln-modal",
778
777
  "description": "Control state of the modal dialog",