@livenetworks/ashlar 1.4.1 → 1.6.0

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 (219) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +189 -99
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -55
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -0
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +14 -5
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +68 -1
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +48 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +47 -0
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +4 -1
  198. package/scss/config/mixins/_ln-table.scss +51 -93
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +232 -38
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +39 -0
  209. package/scss/ln-ashlar.scss +8 -2
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1103
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -168
  218. package/scss/components/_data-table.scss +0 -83
  219. package/scss/config/mixins/_data-table.scss +0 -346
@@ -1,165 +1,536 @@
1
- # `data-ln-data-coordinator`
2
-
3
- A zero-dependency, Local-First **Data Coordinator** component that acts as the orchestrating brain of the 3-Tier Data Layer in `ln-ashlar`.
4
-
5
- This component monitors its DOM subtree, intercepts events, and coordinates the life cycle between a **Local Storage Cache** (`data-ln-data-store`) and any **Transport Gateway** (`data-ln-*-connector`). By orchestrating the flow using event bubbling and a robust Promise-driven chain, it implements the decoupled coordinator pattern, making caching and networking completely agnostic of each other.
6
-
7
- ---
8
-
9
- ## 🧭 The 3-Tier Data Layer Anatomy
10
-
11
- The coordinator acts as a parent wrapper enclosing the database cache and transport connector:
12
-
13
- ```html
14
- <div data-ln-data-coordinator="documents"
15
- data-ln-data-mapper="documents">
16
-
17
- <!-- Tier 1: Local Cache Database (IndexedDB - pure and network-blind) -->
18
- <div data-ln-data-store
19
- data-ln-store-indexes="status,updated_at">
20
- </div>
21
-
22
- <!-- Tier 2: Transport Gateway (API / REST Connector) -->
23
- <div data-ln-api-connector
24
- data-ln-api-base-url="https://api.livenetworks.com/v1"
25
- data-ln-api-path="/documents">
26
- </div>
27
- </div>
28
- ```
29
-
30
- ---
31
-
32
- ## ⚙️ Attributes
33
-
34
- | Attribute | Category | Description |
35
- |-----------|----------|-------------|
36
- | `data-ln-data-coordinator` | Selector | Creates the coordinator instance. The value acts as the domain/scope name (e.g. `documents`). |
37
- | `data-ln-data-mapper` | Mapping | Reference to an externally registered data mapper name (e.g. `documents`). |
38
-
39
- ---
40
-
41
- ## 🔄 Dynamic Child & Mapper Discovery
42
-
43
- The coordinator is built to be highly dynamic, reacting to runtime modifications in its DOM subtree.
44
-
45
- 1. **Child Discovery**: The coordinator automatically locates its child components by querying its DOM subtree:
46
- * **Store Cache**: Looks for `[data-ln-data-store]` and accesses `el.lnDataStore || el.lnStore`.
47
- * **Transport Connector**: Looks for any connector selector (`[data-ln-api-connector]`, `[data-ln-couchdb-connector]`, `[data-ln-websocket-connector]`, `[data-ln-rest-connector]`) and accesses the universal alias `el.lnConnector`.
48
-
49
- 2. **Mapper Resolution**: The coordinator resolves mapping functions using two strategies:
50
- * **Inline Script (Highly Encapsulated)**: Looks for a nested `<script type="application/javascript" data-ln-mapper>` tag in its subtree:
51
- ```html
52
- <script type="application/javascript" data-ln-mapper>
53
- ({
54
- ingress(serverRaw) {
55
- return {
56
- id: serverRaw.id,
57
- title: serverRaw.title,
58
- status: serverRaw.status,
59
- updated_at: Date.parse(serverRaw.updated_at) / 1000
60
- };
61
- },
62
- egress(localDb) {
63
- return {
64
- title: localDb.title,
65
- status: localDb.status
66
- };
67
- }
68
- })
69
- </script>
70
- ```
71
- * **External Registry (Reusability)**: If no inline script exists, it reads the `data-ln-data-mapper` attribute and looks up the mapper via `window.lnCore.getDataMapper(name)`.
72
- * **Fallback**: Defaults to a safe no-op mapper: `{ ingress: r => r, egress: r => r }`.
73
-
74
- ---
75
-
76
- ## ⚡ The Event Loop Orchestration
77
-
78
- Because all events dispatched by the child components bubble up, the coordinator listens directly on its own DOM boundary. It manages the following flows seamlessly:
79
-
80
- ### 1. Delta & Full Sync (`ln-store:request-remote-sync`)
81
- Triggered when the store cache boots up or detects stale data:
82
- ```mermaid
83
- sequenceDiagram
84
- participant Store as data-ln-data-store
85
- participant Coord as data-ln-data-coordinator
86
- participant Gateway as data-ln-*-connector
87
-
88
- Store->>Coord: Event: ln-store:request-remote-sync (since)
89
- Coord->>Gateway: JS API: connector.fetchDelta(since)
90
- Gateway-->>Coord: Promise: returns serverRawResponse
91
- Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(serverRawRecord)
92
- Coord->>Store: JS API: store.applySync(normalizedData, deletedIds, syncedAt)
93
- ```
94
-
95
- ### 2. Optimistic Creation (`ln-store:request-remote-create`)
96
- Triggered when a form submits a new record to the local cache:
97
- ```mermaid
98
- sequenceDiagram
99
- participant Store as data-ln-data-store
100
- participant Coord as data-ln-data-coordinator
101
- participant Gateway as data-ln-*-connector
102
-
103
- Store->>Coord: Event: ln-store:request-remote-create (tempId, data)
104
- Note over Coord: Applies Egress Mapper:<br/>mapper.egress(localRecord)
105
- Coord->>Gateway: JS API: connector.create(serverPayload)
106
- alt Server Success
107
- Gateway-->>Coord: Promise: returns serverRawResponse
108
- Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(serverRaw)
109
- Coord->>Store: JS API: store.confirmMutation(tempId, confirmedLocalRecord, 'create')
110
- else Server Failure
111
- Gateway-->>Coord: Promise: Rejects
112
- Coord->>Store: JS API: store.revertMutation(tempId, 'create', errorMessage)
113
- end
114
- ```
115
-
116
- ### 3. Optimistic Updates (`ln-store:request-remote-update`)
117
- Triggered when a record is updated locally. The coordinator pulls the complete merged record from the database to support standard REST `PUT` schemas:
118
- ```mermaid
119
- sequenceDiagram
120
- participant Store as data-ln-data-store
121
- participant Coord as data-ln-data-coordinator
122
- participant Gateway as data-ln-*-connector
123
-
124
- Store->>Coord: Event: ln-store:request-remote-update (id, expected_version)
125
- Coord->>Store: JS API: store.getById(id)
126
- Store-->>Coord: Returns local record
127
- Note over Coord: Applies Egress Mapper:<br/>mapper.egress(cleanRecord)
128
- Coord->>Gateway: JS API: connector.update(id, serverPayload, expected_version)
129
- alt Server Success
130
- Gateway-->>Coord: Promise: returns serverRawResponse
131
- Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(serverRaw)
132
- Coord->>Store: JS API: store.confirmMutation(id, confirmedLocalRecord, 'update')
133
- else Conflict (409)
134
- Gateway-->>Coord: Promise: Rejects with 409
135
- Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(remoteRecord)
136
- Coord->>Store: JS API: store.resolveConflict(id, remoteRecord, fieldDiffs)
137
- else Other Failure
138
- Gateway-->>Coord: Promise: Rejects
139
- Coord->>Store: JS API: store.revertMutation(id, 'update', errorMessage)
140
- end
141
- ```
142
-
143
- ### 4. Deletions (`ln-store:request-remote-delete` & `bulk-delete`)
144
- Triggered when records are deleted in-memory:
145
- * **Single Delete**: Calls `connector.delete(id)` $\rightarrow$ calls `store.confirmMutation(id, null, 'delete')` (or `revertMutation` on failure).
146
- * **Bulk Delete**: Calls `connector.bulkDelete(ids)` $\rightarrow$ calls `store.confirmMutation(ids.join(','), null, 'bulk-delete')` (or `revertMutation` on failure).
147
-
148
- ---
149
-
150
- ## 💡 JS API (On the element)
151
-
152
- Access the coordinator instance programmatically via the `lnDataCoordinator` or `lnCoordinator` properties:
153
-
154
- ```javascript
155
- const coordinator = document.querySelector('[data-ln-data-coordinator="documents"]').lnDataCoordinator;
156
-
157
- // Force a remote configuration refresh
158
- coordinator.refreshConfig();
159
-
160
- // Retrieve children objects
161
- const { store, connector } = coordinator.findChildren();
162
-
163
- // Fetch currently resolved mapper functions
164
- const { ingress, egress } = coordinator.mapper;
165
- ```
1
+ # `data-ln-data-coordinator`
2
+
3
+ A zero-dependency, Local-First **Data Coordinator** component that orchestrates the full 3-Tier Data Layer in `ln-ashlar`: it bridges the local cache store to remote connectors **and** delivers live data to bound view components (tables, lists, selects, stat counters) with zero application JavaScript.
4
+
5
+ This component monitors its DOM subtree, intercepts events, and coordinates the lifecycle between a **Local Storage Cache** (`data-ln-data-store`) and any **Transport Gateway** (`data-ln-*-connector`). It also listens on `document` for view-binding requests and refreshes all bound view elements on every store mutation.
6
+
7
+ ---
8
+
9
+ ## Declarative View Binding
10
+
11
+ Add these attributes to view elements to bind them to this coordinator's child store. No app JS needed.
12
+
13
+ | Attribute | Applied To | Description |
14
+ |---|---|---|
15
+ | `data-ln-table-store="<storeName>"` | `[data-ln-table]` | Binds a table — receives `ln-table:set-data` automatically. |
16
+ | `data-ln-list-store="<storeName>"` | `[data-ln-list]` | Binds a list — receives `ln-list:set-data` automatically. |
17
+ | `data-ln-options="<storeName>"` | `<select>` | Populated by `ln-options` via `ln-options:set-data`. |
18
+ | `data-ln-stat="<storeName>"` | Inline element | Receives count via `ln-stat:set-count`. |
19
+
20
+ `<storeName>` = the value of `data-ln-data-store` on the coordinator's child store. Multiple coordinators on one page each serve only their own store — isolated by the `_ownsStore(name)` guard.
21
+
22
+ ### Presenter / Binder Split
23
+
24
+ - **Presenters** (`store.setPresenters({computed})`) — registered on the store; computed display fields flow through `getAll → set-data` automatically.
25
+ - **Binder** (this coordinator) — delivers already-decorated records without knowing their shape.
26
+
27
+ Use `setPresenters` for fields like `updated_display`, `size_display`, `status_label`. The binder delivers them as-is.
28
+
29
+ ### Store-Change Refresh
30
+
31
+ The coordinator listens on `this.dom` for `ln-data-store:ready`, `loaded`, `created`, `updated`, `deleted`, and `synced` (only when `changed`). On any of these, `_refreshAll()` re-queries all bound view elements using their last cached query parameters.
32
+
33
+ ### Zero-JS Example
34
+
35
+ ```html
36
+ <ul data-ln-data-coordinator hidden>
37
+ <li data-ln-data-store="people"></li>
38
+ <li data-ln-api-connector data-ln-api-endpoint="/api/people"></li>
39
+ </ul>
40
+
41
+ <section data-ln-table="people"
42
+ data-ln-table-source="people"
43
+ data-ln-table-store="people"
44
+ id="people-table">
45
+ <table>
46
+ <thead>
47
+ <tr>
48
+ <th data-ln-table-col="name">
49
+ Name
50
+ <ul data-ln-sort="people-table" data-ln-sort-field="name" data-ln-sort-state="none">
51
+ <li><button type="button" data-ln-sort-dir="asc" aria-label="Sort ascending"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrows-sort"></use></svg></button></li>
52
+ <li><button type="button" data-ln-sort-dir="desc" aria-label="Sort descending"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrow-up"></use></svg></button></li>
53
+ <li><button type="button" data-ln-sort-dir="none" aria-label="Remove sort"><svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-arrow-down"></use></svg></button></li>
54
+ </ul>
55
+ </th>
56
+ <th data-ln-table-col="status">Status <button data-ln-table-col-filter …></button></th>
57
+ </tr>
58
+ </thead>
59
+ <tbody data-ln-table-body></tbody>
60
+ </table>
61
+ <template data-ln-template="people-row">
62
+ <tr data-ln-table-row>
63
+ <td>{{ name }}</td>
64
+ <td>{{ status_display }}</td>
65
+ </tr>
66
+ </template>
67
+ </section>
68
+
69
+ <select data-ln-options="people" data-ln-options-value="id" data-ln-options-label="name">
70
+ <option value="">All</option>
71
+ </select>
72
+
73
+ <strong data-ln-stat="people" data-ln-stat-filter="status:active"></strong> active
74
+ ```
75
+
76
+ No `<script>` block needed. The coordinator wires everything.
77
+
78
+ ---
79
+
80
+ ## 🧭 The 3-Tier Data Layer Anatomy
81
+
82
+ The coordinator acts as a parent wrapper enclosing the database cache and transport connector:
83
+
84
+ ```html
85
+ <ul data-ln-data-coordinator="documents"
86
+ data-ln-data-mapper="documents" hidden>
87
+
88
+ <!-- Tier 1: Local Cache Database (IndexedDB - pure and network-blind) -->
89
+ <li data-ln-data-store
90
+ data-ln-data-store-indexes="status,updated_at">
91
+ </li>
92
+
93
+ <!-- Tier 2: Transport Gateway (API / REST Connector) -->
94
+ <li data-ln-api-connector
95
+ data-ln-api-base-url="https://api.livenetworks.com/v1"
96
+ data-ln-api-path="/documents">
97
+ </li>
98
+ </ul>
99
+ ```
100
+
101
+ ---
102
+
103
+ ## ⚙️ Attributes
104
+
105
+ | Attribute | Category | Description |
106
+ |-----------|----------|-------------|
107
+ | `data-ln-data-coordinator` | Selector | Creates the coordinator instance. The value acts as the domain/scope name (e.g. `documents`). |
108
+ | `data-ln-data-mapper` | Mapping | Reference to an externally registered data mapper name (e.g. `documents`). |
109
+
110
+ ---
111
+
112
+ ## 🔄 Dynamic Child & Mapper Discovery
113
+
114
+ The coordinator is built to be highly dynamic, reacting to runtime modifications in its DOM subtree.
115
+
116
+ 1. **Child Discovery**: The coordinator automatically locates its child components by querying its DOM subtree:
117
+ * **Store Cache**: Looks for `[data-ln-data-store]` and accesses `el.lnDataStore || el.lnStore`.
118
+ * **Transport Connector**: Looks for any connector selector (`[data-ln-api-connector]`, `[data-ln-couchdb-connector]`, `[data-ln-websocket-connector]`, `[data-ln-rest-connector]`) and accesses the universal alias `el.lnConnector`.
119
+ * **Offline Outbox (optional Child 3)**: Looks for `[data-ln-api-queue]` and accesses `el.lnApiQueue`. When present, write routing (below) enqueues instead of calling the connector directly. When absent, behavior is byte-for-byte the direct-connector path described in this document.
120
+
121
+ 2. **Mapper Resolution**: The coordinator resolves mapping functions using two strategies:
122
+ * **Inline Script (Highly Encapsulated)**: Looks for a nested `<script type="application/javascript" data-ln-mapper>` tag in its subtree:
123
+ ```html
124
+ <script type="application/javascript" data-ln-mapper>
125
+ ({
126
+ ingress(serverRaw) {
127
+ return {
128
+ id: serverRaw.id,
129
+ title: serverRaw.title,
130
+ status: serverRaw.status,
131
+ updated_at: Date.parse(serverRaw.updated_at) / 1000
132
+ };
133
+ },
134
+ egress(localDb) {
135
+ return {
136
+ title: localDb.title,
137
+ status: localDb.status
138
+ };
139
+ }
140
+ })
141
+ </script>
142
+ ```
143
+ * **External Registry (Reusability)**: If no inline script exists, it reads the `data-ln-data-mapper` attribute and looks up the mapper via `window.lnCore.getDataMapper(name)`.
144
+ * **Fallback**: Defaults to a safe no-op mapper: `{ ingress: r => r, egress: r => r }`.
145
+
146
+ ---
147
+
148
+ ## ⚡ The Event Loop Orchestration
149
+
150
+ Because all events dispatched by the child components bubble up, the coordinator listens directly on its own DOM boundary. It manages the following flows seamlessly:
151
+
152
+ ### 0. Form Write Intake (native `submit`, claimed via `preventDefault()`)
153
+
154
+ The coordinator listens for the native `submit` event on `document`
155
+ (bubble phase — never capture, so `ln-validate`'s own validation gate on
156
+ the form itself always runs first). On every submit bubbling through:
157
+
158
+ 1. `if (e.defaultPrevented) return` — either `ln-validate`'s validation
159
+ gate blocked an invalid submit, or another coordinator already claimed it.
160
+ 2. Reads `data-ln-form-scope` off `e.target` (the form). Absent → the
161
+ form never opted in; leave the native submit alone.
162
+ 3. Claims it if either holds: scope value `=== this._name` (named
163
+ override), or the scope is empty and the form is a DOM descendant of
164
+ this coordinator (containment) — identical matching rules as before.
165
+ 4. Resolves the effective method itself (hidden `_method` input if
166
+ present and non-empty, else `form.method`) — a literal read, no
167
+ fallback, mirroring `ln-validate`'s own gate exactly.
168
+ 5. Methods other than `POST`/`PUT`/`PATCH` are left untouched — the
169
+ native submit proceeds (e.g. a `GET` search form nested in the
170
+ coordinator).
171
+ 6. Only now does it call `e.preventDefault()` — this IS the claim, there
172
+ is no separate `claimed` flag to set.
173
+ 7. Serializes the form itself (`serializeForm`, stripping `_method`/`_token`)
174
+ and translates the raw `{ action, method, data }` into the same
175
+ fan-out call as before — `POST` → `_fanOutCreate`; `PUT`/`PATCH` →
176
+ `_fanOutUpdate` (reading `id`/`expected_version` off `data`). The
177
+ coordinator generates the `tempId` itself for a create (no correlation
178
+ map needed — see Fan-Out below). The form's current `action` attribute
179
+ (`form.getAttribute('action')`) rides straight through as the
180
+ mutation's transport `url` — no base/resolved distinction.
181
+
182
+ **Unclaimed scoped forms** (wrong scope name, or not contained in any
183
+ coordinator) fall through as an ordinary native submit — no console
184
+ warning, no silent JS interception. This is the progressive-enhancement
185
+ fallback.
186
+
187
+ ### Coordinator-Namespaced Intake Events
188
+
189
+ The same fan-out is reachable directly (no form involved) via four
190
+ events dispatched on the coordinator's own element:
191
+
192
+ | Event | `detail` |
193
+ |---|---|
194
+ | `ln-data-coordinator:request-create` | `{ data, action? }` |
195
+ | `ln-data-coordinator:request-update` | `{ id, data, expected_version?, action? }` |
196
+ | `ln-data-coordinator:request-delete` | `{ id }` |
197
+ | `ln-data-coordinator:request-bulk-delete` | `{ ids }` |
198
+
199
+ These are intentionally namespaced under the coordinator (not
200
+ `ln-data-store:request-*`) so there is no naming collision with the events the
201
+ store dispatches — no loop-guard logic is needed.
202
+
203
+ ### 1. Parallel Fan-Out — the core write model
204
+
205
+ Every write intake (form or coordinator-namespaced event) reaches one of
206
+ four `_fanOut*` methods. Each one does **two things from the same
207
+ synchronous handler**, independently — the local store write is never
208
+ gated on the remote call succeeding, and vice versa:
209
+
210
+ ```mermaid
211
+ sequenceDiagram
212
+ participant Intake as ln-form / ln-data-coordinator:request-*
213
+ participant Coord as data-ln-data-coordinator
214
+ participant Store as data-ln-data-store
215
+ participant Gateway as data-ln-*-connector / ln-api-queue
216
+
217
+ Intake->>Coord: _fanOutCreate(children, data, action)
218
+ par Local (always)
219
+ Coord->>Store: Event: ln-data-store:request-create { tempId, data }
220
+ Store-->>Coord: Event: ln-data-store:created (bubbles, refreshes bound views)
221
+ and Remote (only if connector or queue child present)
222
+ alt queue present
223
+ Coord->>Gateway: Event: ln-api-queue:request-enqueue { chainKey:tempId, op:'create', payload:egress(data), meta:{tempId,action} }
224
+ else connector present, no queue
225
+ Coord->>Gateway: Event: ln-api-connector:request-create { data:egress(data), url:action, meta:{tempId} }
226
+ end
227
+ end
228
+ ```
229
+
230
+ The only guard on all four write paths is `!children.storeEl` (console
231
+ warning + no-op). There is no `!children.connector` guard — **a
232
+ store-only coordinator (no connector, no queue child) is a valid,
233
+ supported offline-only setup**: the fan-out dispatches the local
234
+ `ln-data-store:request-*` event and stops there. No remote call, no toast; the
235
+ record simply keeps its `_temp_`-prefixed id until a connector is added
236
+ later.
237
+
238
+ ### 2. Sync (`ln-data-store:request-remote-sync`) — unchanged
239
+
240
+ Triggered when the store cache boots up or detects stale data. This is
241
+ the one `request-remote-*` event that still exists — sync has no
242
+ per-record reconciliation, so it was never part of the fan-out/no-pending
243
+ refactor:
244
+
245
+ ```mermaid
246
+ sequenceDiagram
247
+ participant Store as data-ln-data-store
248
+ participant Coord as data-ln-data-coordinator
249
+ participant Gateway as data-ln-*-connector
250
+
251
+ Store->>Coord: Event: ln-data-store:request-remote-sync (since)
252
+ Coord->>Gateway: Event: ln-api-connector:request-sync { since, meta }
253
+ Gateway-->>Coord: Event: ln-api-connector:fetched { data, meta }
254
+ Note over Coord: Applies Ingress Mapper:<br/>mapper.ingress(serverRawRecord)
255
+ Coord->>Store: JS API: store.applySync(normalizedData, deletedIds, syncedAt)
256
+ ```
257
+
258
+ ### 3. Server Confirmation — ordinary update, id-swap on create
259
+
260
+ There is no `confirmMutation`/`revertMutation`/`resolveConflict` anymore.
261
+ A server response is reconciled with **an ordinary
262
+ `ln-data-store:request-update`** — for a create, the target `id` is the
263
+ `tempId` and the incoming `data.id` is the real server id, which the store
264
+ detects as an id-swap (rekey) automatically:
265
+
266
+ ```mermaid
267
+ sequenceDiagram
268
+ participant Gateway as data-ln-*-connector
269
+ participant Coord as data-ln-data-coordinator
270
+ participant Store as data-ln-data-store
271
+
272
+ Gateway-->>Coord: Event: ln-api-connector:created { record, message, meta:{tempId} }
273
+ Note over Coord: mapper.ingress(record)
274
+ Coord->>Store: Event: ln-data-store:request-update { id: meta.tempId, data: serverRecord }
275
+ Note over Store: data.id !== tempId -> rekey (id-swap), fires ln-data-store:updated
276
+ Coord->>Coord: _toastFromMessage(message) — success toast if the server sent one
277
+ ```
278
+
279
+ Update/delete/bulk-delete confirmations follow the same shape (ordinary
280
+ `ln-data-store:request-update`/no reconciliation needed for delete since the
281
+ optimistic delete already applied) — see "Error reconciliation policy"
282
+ below for what happens when the server instead responds with an error.
283
+
284
+ ---
285
+
286
+ ## 📥 Offline Outbox Write Routing (optional `ln-api-queue` child)
287
+
288
+ When a `[data-ln-api-queue]` child is present, every fan-out write
289
+ (`create`/`update`/`delete`/`bulk-delete`) is routed through it instead of
290
+ calling the connector directly. When it is absent, the direct-connector
291
+ flow above applies unchanged. Presence is evaluated per-write inside each
292
+ `_fanOut*` method (`children.queue` takes priority over `children.connector`).
293
+
294
+ **Queue-present enqueue payloads:**
295
+
296
+ | op | `ln-api-queue:request-enqueue` detail |
297
+ |---|---|
298
+ | create | `{ chainKey: tempId, op:'create', targetId:null, payload: egress(data), expectedVersion:null, meta:{ tempId, action } }` |
299
+ | update | `{ chainKey: id, op:'update', targetId: id, payload: egress(data), expectedVersion, meta:{ id, action } }` |
300
+ | delete | `{ chainKey: id, op:'delete', targetId: id, payload:null, expectedVersion:null, meta:{ id } }` |
301
+ | bulk-delete | `{ chainKey: bulkKey, op:'bulk-delete', targetId:null, payload:{ ids }, expectedVersion:null, meta:{ bulkKey, ids } }` |
302
+
303
+ The store is **not** reconciled at enqueue time — reconciliation happens
304
+ only when the queue later commands the coordinator to send.
305
+
306
+ ### Transport executor — `ln-api-queue:send`
307
+
308
+ The queue entry's opaque `meta.action` (the form's resource URL,
309
+ persisted across sessions) now rides into the connector request's `url`
310
+ field at send time; the connector executes the write against it, joined
311
+ with `data-ln-api-base-url` exactly like the `path` fallback.
312
+
313
+ The coordinator listens for `ln-api-queue:send` on the queue element. It
314
+ maps `op` to the matching connector `:request-*` event (same egress/ingress flow as the
315
+ direct path) and, on resolution, drives both the store and the queue via the
316
+ ordinary `ln-data-store:request-update`/`ln-data-store:request-delete` reconciliation
317
+ described above (no `confirmMutation`):
318
+
319
+ | op | success → store | success → queue command |
320
+ |---|---|---|
321
+ | create | Correlated `ln-data-store:request-update { id: meta.tempId, data: ingress(record), requestId }` (id-swap); wait for matching `updated` | `resolve-create {entryId, oldKey:meta.tempId, newId:record.id}` (atomic remap + ack) |
322
+ | update | Correlated `ln-data-store:request-update { id: meta.id, data: ingress(record), requestId }`; wait for matching `updated` | `ack {entryId}` |
323
+ | delete | — (optimistic delete already applied, no reconciliation) | `ack {entryId}` |
324
+ | bulk-delete | — (optimistic delete already applied, no reconciliation) | `ack {entryId}` |
325
+
326
+ After create remap, the queue entry's `targetId` is the authoritative transport
327
+ identity. The coordinator does not fall back to a possibly stale `meta.id`.
328
+
329
+ > Remap also rewrites `meta.action` if it contains the old key as a
330
+ > substring (string replace) — keeps a persisted per-record URL in sync
331
+ > after a create resolves. See `js/ln-api-queue/README.md`.
332
+
333
+ Every success path also calls `_toastFromMessage(e.detail.message)` — see
334
+ Toasts below.
335
+
336
+ Create resolution waits for the correlated store update before advancing the
337
+ outbox. `resolve-create` then deletes the create entry and remaps all queued
338
+ siblings in one IndexedDB transaction, so no sibling can advance with the
339
+ stale temp id.
340
+
341
+ Correlated mutations use one coordinator-owned receipt registry keyed by
342
+ `requestId`. Store post-events settle the matching receipt through the normal
343
+ bubbling event protocol. There are no per-request listeners or fixed timeouts:
344
+ a slow but successful IndexedDB transaction can still acknowledge its queue
345
+ entry, while coordinator teardown rejects all outstanding receipts.
346
+
347
+ Every queued transport request carries `Idempotency-Key: <entryId>` through
348
+ the connector. Delivery remains at-least-once after a crash; a server that
349
+ honours this key can make the mutation effect exactly-once.
350
+
351
+ **On error**, see "Error reconciliation policy" below — the determinism
352
+ classification (auth / transient / deterministic) applies identically on
353
+ the queued and non-queued paths; only the queue command (`nack` reason)
354
+ differs.
355
+
356
+ ---
357
+
358
+ ## 🧭 Local-only mode (no connector child)
359
+
360
+ A coordinator wrapping **only** a `[data-ln-data-store]` child — no
361
+ connector, no queue — is a first-class, supported configuration, not a
362
+ degraded fallback:
363
+
364
+ ```html
365
+ <ul data-ln-data-coordinator="drafts" hidden>
366
+ <li data-ln-data-store="drafts"></li>
367
+ </ul>
368
+ ```
369
+
370
+ Every `_fanOut*` method checks `children.queue` then `children.connector`
371
+ and simply **stops after the local dispatch** if neither exists. Writes
372
+ still go through the coordinator's intake (form or
373
+ `ln-data-coordinator:request-*`), still hit `ln-data-store:request-create`/
374
+ `request-update`/`request-delete`/`request-bulk-delete`, and still refresh
375
+ bound views — there is just no outbound network call and no toast. This is
376
+ the same code path a connector-equipped coordinator uses; adding a
377
+ connector later requires no change to the write intake side.
378
+
379
+ ---
380
+
381
+ ## ⚠️ Error reconciliation policy
382
+
383
+ `connError` classifies every non-2xx connector response into exactly one
384
+ of three buckets by `detail.status` (never a fourth "unknown" bucket —
385
+ `status === 0` is treated as transient, the same as a 5xx):
386
+
387
+ | Bucket | Status | Store action | Queue action (if queued) | Toast |
388
+ |---|---|---|---|---|
389
+ | **Auth** | `401` / `419` | none — optimistic write stays | `nack {reason:'auth'}` (pauses that scope) | `auth` dict key |
390
+ | **Transient** | `0` (network) / `5xx` | none — optimistic write stays; never deleted | `nack {reason:'retry'}` (backoff ladder) | none immediately; on **terminal** failure (`ln-api-queue:failed`) → `network` dict key. Non-queued: `network` dict key fires immediately (single attempt already spent). |
391
+ | **Deterministic** | `409` (update) / other `4xx` / `3xx` | `409` update: ordinary `ln-data-store:request-update` with the server's `remote` record (server wins). `create`: `ln-data-store:request-delete` for the temp id (server rejected it outright). Other 4xx update/delete/bulk: left as-is, next sync reconciles. | `nack {reason:'drop'}` (never retried) | `conflict` (409 update) or `rejected` (everything else deterministic) |
392
+
393
+ **Never retried** = deterministic errors always `nack 'drop'` on the queued
394
+ path — a 4xx is never re-attempted, only sync eventually catches up.
395
+ **Never silently discards a local write on a transient/network failure** —
396
+ that is the core "no-pending" invariant: an optimistic write is only ever
397
+ removed by an explicit server rejection (create 4xx) or a genuine local
398
+ `ln-data-store:request-delete`, never by a retry-exhausted network error.
399
+
400
+ There is no `ln-data-store:sync-conflict` event and no `forceSync()` error
401
+ backstop — both were removed as part of this refactor (no grep-verified
402
+ consumer existed for either).
403
+
404
+ ---
405
+
406
+ ## 🔔 Toasts
407
+
408
+ The `ln-store-notify` component has been removed. Toasts now come from two
409
+ independent sources, both funneled through the standard `ln-toast:enqueue`
410
+ window event (consumed by `ln-toast` if present on the page, silently
411
+ ignored otherwise):
412
+
413
+ 1. **Success — from the server's response envelope.** When a connector
414
+ mutation response includes a `message` (see the
415
+ [ln-api-connector README](../ln-api-connector/README.md) → Mutation
416
+ Response Envelope), the coordinator's `_toastFromMessage(message)`
417
+ enqueues it verbatim (`type`/`title`/`body` from the server, defaulting
418
+ `type` to `'success'`). **No `message` → no toast.** This mirrors
419
+ `ln-ajax`'s existing `data.message` → `ln-toast:enqueue` precedent
420
+ exactly.
421
+ 2. **Error — from a coordinator markup dictionary.** Error text is never
422
+ hardcoded in JS. It is authored once per coordinator instance via
423
+ `data-ln-data-coordinator-dict` child elements (parsed with the same
424
+ `buildDict()` helper `ln-upload` uses for its dictionary):
425
+
426
+ ```html
427
+ <ul data-ln-data-coordinator="documents" hidden>
428
+ <li data-ln-data-store="documents"></li>
429
+ <li data-ln-api-connector data-ln-api-base-url="/api" data-ln-api-path="/documents"></li>
430
+
431
+ <!-- consumed once at init, then removed from the DOM -->
432
+ <span data-ln-data-coordinator-dict="auth" hidden>Your session expired — please sign in again.</span>
433
+ <span data-ln-data-coordinator-dict="network" hidden>Could not reach the server — your change is saved locally and will retry.</span>
434
+ <span data-ln-data-coordinator-dict="conflict" hidden>Someone else updated this record — showing their version.</span>
435
+ <span data-ln-data-coordinator-dict="rejected" hidden>The server rejected that change.</span>
436
+ </ul>
437
+ ```
438
+
439
+ Keys are exactly the four buckets from the error reconciliation table:
440
+ `auth`, `network`, `conflict`, `rejected`. **A missing key is silent —
441
+ no fallback string is ever synthesized.** If the coordinator has no
442
+ dictionary at all, `this._dict` is `{}` and every `_toastFromDict` call
443
+ is a no-op.
444
+
445
+ ### Sync ownership
446
+
447
+ The coordinator — not the store — decides WHEN to sync:
448
+
449
+ * Listens for `ln-data-store:initialized` on its store child: if
450
+ `!detail.hasCache` → `store.forceSync()` (initial load); else if the
451
+ store is stale → `store.forceSync()`.
452
+ * **Race guard.** `ln-data-store:initialized` can fire before the coordinator
453
+ finishes binding (async `_initStore`, SPA-injected subtree). When
454
+ children resolve, if `store.isLoaded` is already true, the coordinator
455
+ evaluates the same condition directly from instance state instead of
456
+ relying on the event alone.
457
+ * A module-level singleton wires **one shared** `window 'online'`,
458
+ `window 'offline'`, and `document 'visibilitychange'` listener set across
459
+ every coordinator instance on the page (not one per instance). On
460
+ `online`, dispatches `ln-data-store:online` on `document` once, then for each
461
+ coordinator with a loaded, non-syncing store: `store.forceSync()`. On
462
+ `offline`, dispatches `ln-data-store:offline` on `document` once. On
463
+ `visibilitychange` (tab visible again), for each coordinator whose store
464
+ is stale: `store.forceSync()`.
465
+
466
+ If IndexedDB initialization fails, the coordinator does not start a cache sync.
467
+ The store exposes the failure as `initializationError`, and connector-backed
468
+ view reads use the remote source directly.
469
+
470
+ **New attributes:**
471
+
472
+ | Attribute | Description |
473
+ |---|---|
474
+ | `data-ln-data-coordinator-stale` | Seconds threshold before the store is considered stale; falls back to the store's own `data-ln-data-store-stale`; default 300; `-1` / `never` = never stale. |
475
+ | `data-ln-data-coordinator-no-autosync` | Presence opts the coordinator out of online/visibility auto-sync; falls back to the store's own `data-ln-data-store-no-autosync`. |
476
+
477
+ `ln-data-store:online` / `ln-data-store:offline` now require a coordinator on the
478
+ page — the store itself no longer dispatches them (see the
479
+ [ln-data-store README](../ln-data-store/README.md)).
480
+
481
+ ---
482
+
483
+ ## 💡 JS API (On the element)
484
+
485
+ Access the coordinator instance programmatically via the `lnDataCoordinator` or `lnCoordinator` properties:
486
+
487
+ ```javascript
488
+ const coordinator = document.querySelector('[data-ln-data-coordinator="documents"]').lnDataCoordinator;
489
+
490
+ // Force a remote configuration refresh
491
+ coordinator.refreshConfig();
492
+
493
+ // Retrieve children objects (queue is null when no [data-ln-api-queue] child exists)
494
+ const { store, connector, queue } = coordinator.findChildren();
495
+
496
+ // Fetch currently resolved mapper functions
497
+ const { ingress, egress } = coordinator.mapper;
498
+ ```
499
+
500
+ ---
501
+
502
+ ## 🔧 Internals — View-Binding Layer
503
+
504
+ Source: `js/ln-data-coordinator/src/ln-data-coordinator.js`. The write pipeline (fan-out, sync, queue, toasts) is documented above; this section covers the coordinator's second role — serving live data to bound view components with zero application JS.
505
+
506
+ ### Binding attributes
507
+
508
+ | Attribute | Applied to | Served by |
509
+ |---|---|---|
510
+ | `data-ln-table-store="<name>"` | `[data-ln-table]` | `ln-table:set-data` |
511
+ | `data-ln-list-store="<name>"` | `[data-ln-list]` | `ln-list:set-data` |
512
+ | `data-ln-options="<name>"` | `<select>` | `ln-options:set-data` |
513
+ | `data-ln-stat="<name>"` | inline element | `ln-stat:set-count` |
514
+
515
+ `<name>` must match the coordinator's own child store's `data-ln-data-store` value. Every document-level handler guards with `_ownsStore(name)` first (`children.store && children.store._name === name`), so multiple coordinators on one page coexist without cross-talk.
516
+
517
+ ### Presenter / binder split
518
+
519
+ Presenters (`store.setPresenters({computed})`) are registered once per store; computed display fields (`updated_display`, `status_label`, …) flow through `getAll → set-data` automatically. The coordinator is a pure binder — it transports whatever `getAll` produces to the requesting view element without transforming it.
520
+
521
+ The binder awaits `store.ready` before choosing a source. Loaded non-windowed
522
+ reads use the local store; an empty local-only store is also a valid source and
523
+ returns `[]`. Windowed reads, connector-backed stores without a loaded cache,
524
+ and stores with `initializationError` route remote. When IndexedDB is
525
+ unavailable, table/list/options/stat responses are delivered directly to the
526
+ requesting view instead of being forced through the failed store. Query
527
+ failures clear loading and emit
528
+ `ln-data-coordinator:error {operation:'query', kind, store, target, error}`.
529
+
530
+ ### Refresh trigger
531
+
532
+ On `ln-data-store:ready`/`loaded`/`created`/`updated`/`deleted`, and `synced` (only when `e.detail.changed`), `_refreshAll()` re-queries every bound view element using its last cached query parameters (or defaults) — so a filter/sort/search a user set on a table survives a background sync.
533
+
534
+ ### `data-ln-table-filter-options` — removed
535
+
536
+ Filter options are now static authored markup inside `[data-ln-popover]` blocks (domain-owned enum/lookup), not accumulated from the dataset. See `docs/architecture/reference.md#column-filter-architecture`.