@zintrust/workers 0.1.27

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 (178) hide show
  1. package/README.md +861 -0
  2. package/dist/AnomalyDetection.d.ts +102 -0
  3. package/dist/AnomalyDetection.js +321 -0
  4. package/dist/AutoScaler.d.ts +127 -0
  5. package/dist/AutoScaler.js +425 -0
  6. package/dist/BroadcastWorker.d.ts +21 -0
  7. package/dist/BroadcastWorker.js +24 -0
  8. package/dist/CanaryController.d.ts +103 -0
  9. package/dist/CanaryController.js +380 -0
  10. package/dist/ChaosEngineering.d.ts +79 -0
  11. package/dist/ChaosEngineering.js +216 -0
  12. package/dist/CircuitBreaker.d.ts +106 -0
  13. package/dist/CircuitBreaker.js +374 -0
  14. package/dist/ClusterLock.d.ts +90 -0
  15. package/dist/ClusterLock.js +385 -0
  16. package/dist/ComplianceManager.d.ts +177 -0
  17. package/dist/ComplianceManager.js +556 -0
  18. package/dist/DatacenterOrchestrator.d.ts +133 -0
  19. package/dist/DatacenterOrchestrator.js +404 -0
  20. package/dist/DeadLetterQueue.d.ts +122 -0
  21. package/dist/DeadLetterQueue.js +539 -0
  22. package/dist/HealthMonitor.d.ts +42 -0
  23. package/dist/HealthMonitor.js +301 -0
  24. package/dist/MultiQueueWorker.d.ts +89 -0
  25. package/dist/MultiQueueWorker.js +277 -0
  26. package/dist/NotificationWorker.d.ts +21 -0
  27. package/dist/NotificationWorker.js +23 -0
  28. package/dist/Observability.d.ts +153 -0
  29. package/dist/Observability.js +530 -0
  30. package/dist/PluginManager.d.ts +123 -0
  31. package/dist/PluginManager.js +392 -0
  32. package/dist/PriorityQueue.d.ts +117 -0
  33. package/dist/PriorityQueue.js +244 -0
  34. package/dist/ResourceMonitor.d.ts +164 -0
  35. package/dist/ResourceMonitor.js +605 -0
  36. package/dist/SLAMonitor.d.ts +110 -0
  37. package/dist/SLAMonitor.js +274 -0
  38. package/dist/WorkerFactory.d.ts +193 -0
  39. package/dist/WorkerFactory.js +1507 -0
  40. package/dist/WorkerInit.d.ts +85 -0
  41. package/dist/WorkerInit.js +223 -0
  42. package/dist/WorkerMetrics.d.ts +114 -0
  43. package/dist/WorkerMetrics.js +509 -0
  44. package/dist/WorkerRegistry.d.ts +145 -0
  45. package/dist/WorkerRegistry.js +319 -0
  46. package/dist/WorkerShutdown.d.ts +61 -0
  47. package/dist/WorkerShutdown.js +159 -0
  48. package/dist/WorkerVersioning.d.ts +107 -0
  49. package/dist/WorkerVersioning.js +300 -0
  50. package/dist/build-manifest.json +462 -0
  51. package/dist/config/workerConfig.d.ts +3 -0
  52. package/dist/config/workerConfig.js +19 -0
  53. package/dist/createQueueWorker.d.ts +23 -0
  54. package/dist/createQueueWorker.js +113 -0
  55. package/dist/dashboard/index.d.ts +1 -0
  56. package/dist/dashboard/index.js +1 -0
  57. package/dist/dashboard/types.d.ts +117 -0
  58. package/dist/dashboard/types.js +1 -0
  59. package/dist/dashboard/workers-api.d.ts +4 -0
  60. package/dist/dashboard/workers-api.js +638 -0
  61. package/dist/dashboard/workers-dashboard-ui.d.ts +3 -0
  62. package/dist/dashboard/workers-dashboard-ui.js +1026 -0
  63. package/dist/dashboard/workers-dashboard.d.ts +4 -0
  64. package/dist/dashboard/workers-dashboard.js +904 -0
  65. package/dist/helper/index.d.ts +5 -0
  66. package/dist/helper/index.js +10 -0
  67. package/dist/http/WorkerApiController.d.ts +38 -0
  68. package/dist/http/WorkerApiController.js +312 -0
  69. package/dist/http/WorkerController.d.ts +374 -0
  70. package/dist/http/WorkerController.js +1351 -0
  71. package/dist/http/middleware/CustomValidation.d.ts +92 -0
  72. package/dist/http/middleware/CustomValidation.js +270 -0
  73. package/dist/http/middleware/DatacenterValidator.d.ts +3 -0
  74. package/dist/http/middleware/DatacenterValidator.js +94 -0
  75. package/dist/http/middleware/EditWorkerValidation.d.ts +7 -0
  76. package/dist/http/middleware/EditWorkerValidation.js +55 -0
  77. package/dist/http/middleware/FeaturesValidator.d.ts +3 -0
  78. package/dist/http/middleware/FeaturesValidator.js +60 -0
  79. package/dist/http/middleware/InfrastructureValidator.d.ts +31 -0
  80. package/dist/http/middleware/InfrastructureValidator.js +226 -0
  81. package/dist/http/middleware/OptionsValidator.d.ts +3 -0
  82. package/dist/http/middleware/OptionsValidator.js +112 -0
  83. package/dist/http/middleware/PayloadSanitizer.d.ts +7 -0
  84. package/dist/http/middleware/PayloadSanitizer.js +42 -0
  85. package/dist/http/middleware/ProcessorPathSanitizer.d.ts +3 -0
  86. package/dist/http/middleware/ProcessorPathSanitizer.js +74 -0
  87. package/dist/http/middleware/QueueNameSanitizer.d.ts +3 -0
  88. package/dist/http/middleware/QueueNameSanitizer.js +45 -0
  89. package/dist/http/middleware/ValidateDriver.d.ts +7 -0
  90. package/dist/http/middleware/ValidateDriver.js +20 -0
  91. package/dist/http/middleware/VersionSanitizer.d.ts +3 -0
  92. package/dist/http/middleware/VersionSanitizer.js +25 -0
  93. package/dist/http/middleware/WorkerNameSanitizer.d.ts +3 -0
  94. package/dist/http/middleware/WorkerNameSanitizer.js +46 -0
  95. package/dist/http/middleware/WorkerValidationChain.d.ts +27 -0
  96. package/dist/http/middleware/WorkerValidationChain.js +185 -0
  97. package/dist/index.d.ts +46 -0
  98. package/dist/index.js +48 -0
  99. package/dist/routes/workers.d.ts +12 -0
  100. package/dist/routes/workers.js +81 -0
  101. package/dist/storage/WorkerStore.d.ts +45 -0
  102. package/dist/storage/WorkerStore.js +195 -0
  103. package/dist/type.d.ts +76 -0
  104. package/dist/type.js +1 -0
  105. package/dist/ui/router/ui.d.ts +3 -0
  106. package/dist/ui/router/ui.js +83 -0
  107. package/dist/ui/types/worker-ui.d.ts +229 -0
  108. package/dist/ui/types/worker-ui.js +5 -0
  109. package/package.json +53 -0
  110. package/src/AnomalyDetection.ts +434 -0
  111. package/src/AutoScaler.ts +654 -0
  112. package/src/BroadcastWorker.ts +34 -0
  113. package/src/CanaryController.ts +531 -0
  114. package/src/ChaosEngineering.ts +301 -0
  115. package/src/CircuitBreaker.ts +495 -0
  116. package/src/ClusterLock.ts +499 -0
  117. package/src/ComplianceManager.ts +815 -0
  118. package/src/DatacenterOrchestrator.ts +561 -0
  119. package/src/DeadLetterQueue.ts +733 -0
  120. package/src/HealthMonitor.ts +390 -0
  121. package/src/MultiQueueWorker.ts +431 -0
  122. package/src/NotificationWorker.ts +33 -0
  123. package/src/Observability.ts +696 -0
  124. package/src/PluginManager.ts +551 -0
  125. package/src/PriorityQueue.ts +351 -0
  126. package/src/ResourceMonitor.ts +769 -0
  127. package/src/SLAMonitor.ts +408 -0
  128. package/src/WorkerFactory.ts +2108 -0
  129. package/src/WorkerInit.ts +313 -0
  130. package/src/WorkerMetrics.ts +709 -0
  131. package/src/WorkerRegistry.ts +443 -0
  132. package/src/WorkerShutdown.ts +210 -0
  133. package/src/WorkerVersioning.ts +422 -0
  134. package/src/config/workerConfig.ts +25 -0
  135. package/src/createQueueWorker.ts +174 -0
  136. package/src/dashboard/index.ts +6 -0
  137. package/src/dashboard/types.ts +141 -0
  138. package/src/dashboard/workers-api.ts +785 -0
  139. package/src/dashboard/zintrust.svg +30 -0
  140. package/src/helper/index.ts +11 -0
  141. package/src/http/WorkerApiController.ts +369 -0
  142. package/src/http/WorkerController.ts +1512 -0
  143. package/src/http/middleware/CustomValidation.ts +360 -0
  144. package/src/http/middleware/DatacenterValidator.ts +124 -0
  145. package/src/http/middleware/EditWorkerValidation.ts +74 -0
  146. package/src/http/middleware/FeaturesValidator.ts +82 -0
  147. package/src/http/middleware/InfrastructureValidator.ts +295 -0
  148. package/src/http/middleware/OptionsValidator.ts +144 -0
  149. package/src/http/middleware/PayloadSanitizer.ts +52 -0
  150. package/src/http/middleware/ProcessorPathSanitizer.ts +86 -0
  151. package/src/http/middleware/QueueNameSanitizer.ts +55 -0
  152. package/src/http/middleware/ValidateDriver.ts +29 -0
  153. package/src/http/middleware/VersionSanitizer.ts +30 -0
  154. package/src/http/middleware/WorkerNameSanitizer.ts +56 -0
  155. package/src/http/middleware/WorkerValidationChain.ts +230 -0
  156. package/src/index.ts +98 -0
  157. package/src/routes/workers.ts +154 -0
  158. package/src/storage/WorkerStore.ts +240 -0
  159. package/src/type.ts +89 -0
  160. package/src/types/queue-monitor.d.ts +38 -0
  161. package/src/types/queue-redis.d.ts +38 -0
  162. package/src/ui/README.md +13 -0
  163. package/src/ui/components/JsonEditor.js +670 -0
  164. package/src/ui/components/JsonViewer.js +387 -0
  165. package/src/ui/components/WorkerCard.js +178 -0
  166. package/src/ui/components/WorkerExpandPanel.js +257 -0
  167. package/src/ui/components/fetcher.js +42 -0
  168. package/src/ui/components/sla-scorecard.js +32 -0
  169. package/src/ui/components/styles.css +30 -0
  170. package/src/ui/components/table-expander.js +34 -0
  171. package/src/ui/integration/worker-ui-integration.js +565 -0
  172. package/src/ui/router/ui.ts +99 -0
  173. package/src/ui/services/workerApi.js +240 -0
  174. package/src/ui/types/worker-ui.ts +283 -0
  175. package/src/ui/utils/jsonValidator.js +444 -0
  176. package/src/ui/workers/index.html +202 -0
  177. package/src/ui/workers/main.js +1781 -0
  178. package/src/ui/workers/styles.css +1350 -0
@@ -0,0 +1,257 @@
1
+ /* eslint-disable no-restricted-syntax */
2
+ /* eslint-disable no-undef */
3
+ /* eslint-disable @typescript-eslint/explicit-function-return-type */
4
+ /**
5
+ * Worker Expand Panel Component
6
+ * Displays detailed worker information with View JSON and Edit buttons
7
+ */
8
+
9
+ /**
10
+ * Create configuration summary element
11
+ * @param {Object} worker - Worker data
12
+ * @returns {HTMLDivElement} Configuration summary element
13
+ */
14
+ const createConfigSummaryElement = (worker) => {
15
+ const configSummary = document.createElement('div');
16
+ configSummary.className = 'config-summary';
17
+
18
+ const title = document.createElement('h4');
19
+ title.textContent = 'Configuration';
20
+ configSummary.appendChild(title);
21
+
22
+ const configGrid = document.createElement('div');
23
+ configGrid.className = 'config-grid';
24
+
25
+ const configItems = [
26
+ { label: 'Driver:', value: worker.driver },
27
+ { label: 'Concurrency:', value: worker.concurrency },
28
+ { label: 'Auto Start:', value: worker.autoStart ? 'Yes' : 'No' },
29
+ { label: 'Version:', value: worker.version || 'N/A' },
30
+ { label: 'Region:', value: worker.region || 'N/A' },
31
+ { label: 'Created:', value: new Date(worker.createdAt).toLocaleString() },
32
+ ];
33
+
34
+ configItems.forEach((item) => {
35
+ const configItem = document.createElement('div');
36
+ configItem.className = 'config-item';
37
+
38
+ const label = document.createElement('label');
39
+ label.textContent = item.label;
40
+
41
+ const span = document.createElement('span');
42
+ span.textContent = item.value;
43
+
44
+ configItem.appendChild(label);
45
+ configItem.appendChild(span);
46
+ configGrid.appendChild(configItem);
47
+ });
48
+
49
+ configSummary.appendChild(configGrid);
50
+ return configSummary;
51
+ };
52
+
53
+ /**
54
+ * Create action buttons section
55
+ * @returns {HTMLDivElement} Action buttons element
56
+ */
57
+ const createActionButtonsSection = () => {
58
+ const actionsSection = document.createElement('div');
59
+ actionsSection.className = 'worker-actions-section';
60
+
61
+ const viewJsonBtn = document.createElement('button');
62
+ viewJsonBtn.className = 'btn btn-secondary view-json-btn';
63
+ viewJsonBtn.textContent = 'View JSON';
64
+
65
+ const editJsonBtn = document.createElement('button');
66
+ editJsonBtn.className = 'btn btn-primary edit-json-btn';
67
+ editJsonBtn.textContent = 'Edit';
68
+
69
+ actionsSection.appendChild(viewJsonBtn);
70
+ actionsSection.appendChild(editJsonBtn);
71
+
72
+ return actionsSection;
73
+ };
74
+
75
+ /**
76
+ * Create panel element
77
+ * @param {Object} worker - Worker data
78
+ * @returns {HTMLDivElement} Panel element
79
+ */
80
+ const createPanelElement = (worker) => {
81
+ const panelElement = document.createElement('div');
82
+ panelElement.className = 'worker-expand-panel';
83
+
84
+ const detailsSection = document.createElement('div');
85
+ detailsSection.className = 'worker-details';
86
+
87
+ const configSummary = createConfigSummaryElement(worker);
88
+
89
+ const actionsSection = createActionButtonsSection();
90
+
91
+ detailsSection.appendChild(configSummary);
92
+ detailsSection.appendChild(actionsSection);
93
+ panelElement.appendChild(detailsSection);
94
+
95
+ const metricsSection = document.createElement('div');
96
+ metricsSection.className = 'worker-metrics';
97
+ metricsSection.innerHTML = `
98
+ <h4>Performance Metrics</h4>
99
+ <div class="metrics-grid">
100
+ <div class="metric-item">
101
+ <label>Status:</label>
102
+ <span class="status-indicator status-${worker.status}">${worker.status}</span>
103
+ </div>
104
+ <div class="metric-item">
105
+ <label>Connection:</label>
106
+ <span class="connection-indicator">${worker.connectionState || 'Unknown'}</span>
107
+ </div>
108
+ <div class="metric-item">
109
+ <label>Last Health Check:</label>
110
+ <span>${worker.lastHealthCheck ? new Date(worker.lastHealthCheck).toLocaleString() : 'Never'}</span>
111
+ </div>
112
+ ${
113
+ worker.lastError
114
+ ? `
115
+ <div class="metric-item">
116
+ <label>Last Error:</label>
117
+ <span class="error-message">${worker.lastError}</span>
118
+ </div>
119
+ `
120
+ : ''
121
+ }
122
+ </div>
123
+ `;
124
+ panelElement.appendChild(metricsSection);
125
+
126
+ return panelElement;
127
+ };
128
+
129
+ /**
130
+ * Show notification message with proper timeout cleanup
131
+ * @param {string} message - Notification message
132
+ * @param {string} type - Notification type
133
+ * @returns {number} Timeout reference for cleanup
134
+ */
135
+ const showNotification = (message, type = 'info') => {
136
+ const notification = document.createElement('div');
137
+ notification.className = `notification notification-${type}`;
138
+ notification.textContent = message;
139
+
140
+ document.body.appendChild(notification);
141
+
142
+ const timeoutId = setTimeout(() => {
143
+ notification.remove();
144
+ activeTimeouts.delete(timeoutId); // Clean up from global registry
145
+ }, 3000);
146
+
147
+ activeTimeouts.add(timeoutId); // Track timeout for cleanup
148
+ return timeoutId;
149
+ };
150
+
151
+ /**
152
+ * Create JSON viewer handler
153
+ * @param {Object} worker - Worker data
154
+ * @returns {Function} View JSON handler
155
+ */
156
+ const createViewJsonHandler = (worker) => {
157
+ let jsonViewer = null;
158
+
159
+ return () => {
160
+ if (!jsonViewer) {
161
+ jsonViewer = JsonViewer.create();
162
+ }
163
+ jsonViewer.open(worker);
164
+ };
165
+ };
166
+
167
+ /**
168
+ * Create JSON editor handler
169
+ * @param {Object} worker - Worker data
170
+ * @param {Function} render - Render function
171
+ * @returns {Function} Edit JSON handler
172
+ */
173
+ const createEditJsonHandler = (worker, render) => {
174
+ let jsonEditor = null;
175
+
176
+ return async () => {
177
+ if (!jsonEditor) {
178
+ jsonEditor = JsonEditor.create({
179
+ onSave: async (updatedWorker) => {
180
+ try {
181
+ const response = await workerApi.updateWorker(worker.id, updatedWorker);
182
+
183
+ if (response.success) {
184
+ Object.assign(worker, response.data);
185
+ render();
186
+ showNotification('Worker updated successfully', 'success');
187
+ } else {
188
+ showNotification(response.error || 'Failed to update worker', 'error');
189
+ }
190
+ } catch (error) {
191
+ showNotification('Error updating worker: ' + error.message, 'error');
192
+ }
193
+ },
194
+ });
195
+ }
196
+ jsonEditor.open(worker);
197
+ };
198
+ };
199
+
200
+ /**
201
+ * Create a worker expand panel
202
+ */
203
+ const createWorkerExpandPanel = (worker, container) => {
204
+ let element;
205
+ const jsonViewer = null;
206
+ const jsonEditor = null;
207
+
208
+ const render = () => {
209
+ if (element) {
210
+ element.remove();
211
+ }
212
+ element = createPanelElement(worker);
213
+ container.appendChild(element);
214
+
215
+ // Create and attach event listeners after element is created
216
+ const handleViewJson = createViewJsonHandler(worker);
217
+ const handleEditJson = createEditJsonHandler(worker, render);
218
+
219
+ // Search within the current panel element, not the entire document
220
+ const viewJsonBtn = element?.querySelector('.view-json-btn');
221
+ const editJsonBtn = element?.querySelector('.edit-json-btn');
222
+
223
+ if (viewJsonBtn) {
224
+ viewJsonBtn.addEventListener('click', handleViewJson);
225
+ }
226
+ if (editJsonBtn) {
227
+ editJsonBtn.addEventListener('click', handleEditJson);
228
+ }
229
+ };
230
+
231
+ const destroy = () => {
232
+ if (jsonViewer) {
233
+ jsonViewer.destroy();
234
+ }
235
+ if (jsonEditor) {
236
+ jsonEditor.destroy();
237
+ }
238
+ element.remove();
239
+ };
240
+
241
+ // Initialize
242
+ render();
243
+
244
+ return {
245
+ element,
246
+ worker,
247
+ render,
248
+ destroy,
249
+ };
250
+ };
251
+
252
+ /**
253
+ * Sealed namespace for WorkerExpandPanel utilities
254
+ */
255
+ export const WorkerExpandPanel = Object.freeze({
256
+ create: createWorkerExpandPanel,
257
+ });
@@ -0,0 +1,42 @@
1
+ /* eslint-disable @typescript-eslint/explicit-function-return-type */
2
+ /* eslint-disable no-restricted-syntax */
3
+
4
+ // Helper function for retry delay
5
+ function delay(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+
9
+ export async function get(path, options = {}) {
10
+ const retries = options.retry ?? 2;
11
+ const backoff = options.backoff ?? 200;
12
+
13
+ async function fetchWithRetry(attempt = 0) {
14
+ try {
15
+ const res = await fetch(path, { cache: 'no-store' });
16
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
17
+ return await res.json();
18
+ } catch (err) {
19
+ if (attempt >= retries) throw err;
20
+
21
+ // Wait before retry (exponential backoff)
22
+ const waitTime = backoff * (attempt + 1);
23
+ await delay(waitTime);
24
+
25
+ // Recursive call for next attempt
26
+ return fetchWithRetry(attempt + 1);
27
+ }
28
+ }
29
+
30
+ return fetchWithRetry();
31
+ }
32
+
33
+ export async function post(path, body = {}, options = {}) {
34
+ const res = await fetch(path, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify(body),
38
+ ...options,
39
+ });
40
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
41
+ return res.json().catch(() => ({ ok: res.ok }));
42
+ }
@@ -0,0 +1,32 @@
1
+ /* eslint-disable @typescript-eslint/explicit-function-return-type */
2
+ /* global document */
3
+
4
+ export function renderSlaScorecard(container, payload) {
5
+ if (!container) return;
6
+ const status = payload?.status || 'unknown';
7
+ const score = payload?.score ?? '-';
8
+
9
+ // Clear existing content safely
10
+ while (container.firstChild) {
11
+ container.firstChild.remove();
12
+ }
13
+
14
+ // Create main card element
15
+ const card = document.createElement('div');
16
+ card.className = `ui-sla-card sla-${status}`;
17
+
18
+ // Create score element
19
+ const scoreEl = document.createElement('div');
20
+ scoreEl.className = 'ui-sla-score';
21
+ scoreEl.textContent = score; // Safe: textContent doesn't execute HTML
22
+
23
+ // Create status element
24
+ const statusEl = document.createElement('div');
25
+ statusEl.className = 'ui-sla-status';
26
+ statusEl.textContent = String(status).toUpperCase(); // Safe: textContent
27
+
28
+ // Assemble the card
29
+ card.appendChild(scoreEl);
30
+ card.appendChild(statusEl);
31
+ container.appendChild(card);
32
+ }
@@ -0,0 +1,30 @@
1
+ :root {
2
+ --zt-bg: #0b1220;
3
+ --zt-card: #0f1724;
4
+ --zt-text: #e6eef6;
5
+ --zt-muted: #94a3b8;
6
+ --zt-accent: #38bdf8;
7
+ }
8
+ .ui-loading {
9
+ color: var(--zt-muted);
10
+ padding: 12px;
11
+ }
12
+ .ui-detail-row td {
13
+ background: var(--zt-card);
14
+ padding: 12px;
15
+ border-top: 1px solid rgba(255, 255, 255, 0.03);
16
+ }
17
+ .ui-sla-card {
18
+ border-radius: 8px;
19
+ padding: 12px;
20
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0.08));
21
+ display: inline-block;
22
+ }
23
+ .ui-sla-score {
24
+ font-size: 20px;
25
+ font-weight: 800;
26
+ }
27
+ .ui-sla-status {
28
+ font-size: 11px;
29
+ color: var(--zt-muted);
30
+ }
@@ -0,0 +1,34 @@
1
+ /* eslint-disable no-restricted-syntax */
2
+ /* eslint-disable no-undef */
3
+ /* eslint-disable @typescript-eslint/explicit-function-return-type */
4
+
5
+ function removeDetail(detailsEl) {
6
+ if (detailsEl) detailsEl.remove();
7
+ }
8
+
9
+ function renderRow(rowEl, detailHtml) {
10
+ const details = document.createElement('tr');
11
+ details.className = 'ui-detail-row';
12
+ const td = document.createElement('td');
13
+ td.colSpan = rowEl.children.length;
14
+ td.innerHTML = detailHtml || '';
15
+ details.appendChild(td);
16
+ rowEl.after(details);
17
+ return details;
18
+ }
19
+
20
+ export function createTableExpander(container) {
21
+ if (!container) throw new Error('container required');
22
+
23
+ function toggle(rowEl, loader) {
24
+ const next = rowEl.nextElementSibling;
25
+ if (next && next.classList.contains('ui-detail-row')) {
26
+ removeDetail(next);
27
+ return null;
28
+ }
29
+ const placeholder = renderRow(rowEl, loader || '<div class="ui-loading">Loading...</div>');
30
+ return placeholder;
31
+ }
32
+
33
+ return Object.freeze({ toggle, renderRow, removeDetail });
34
+ }