@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,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",