@loadbare/app 0.4.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.
- package/LICENSE +201 -0
- package/README.md +95 -0
- package/dist/build/assemble.d.ts +31 -0
- package/dist/build/assemble.d.ts.map +1 -0
- package/dist/build/assemble.js +53 -0
- package/dist/build/cli.d.ts +30 -0
- package/dist/build/cli.d.ts.map +1 -0
- package/dist/build/cli.js +107 -0
- package/dist/build/elements.d.ts +61 -0
- package/dist/build/elements.d.ts.map +1 -0
- package/dist/build/elements.js +158 -0
- package/dist/build/expand.d.ts +45 -0
- package/dist/build/expand.d.ts.map +1 -0
- package/dist/build/expand.js +386 -0
- package/dist/build/format.d.ts +28 -0
- package/dist/build/format.d.ts.map +1 -0
- package/dist/build/format.js +42 -0
- package/dist/build/locations.d.ts +87 -0
- package/dist/build/locations.d.ts.map +1 -0
- package/dist/build/locations.js +173 -0
- package/dist/build/package-root.d.ts +9 -0
- package/dist/build/package-root.d.ts.map +1 -0
- package/dist/build/package-root.js +24 -0
- package/dist/build/pages.d.ts +25 -0
- package/dist/build/pages.d.ts.map +1 -0
- package/dist/build/pages.js +54 -0
- package/dist/build/styles.d.ts +13 -0
- package/dist/build/styles.d.ts.map +1 -0
- package/dist/build/styles.js +18 -0
- package/dist/client.js +522 -0
- package/dist/core/lb-constants.d.ts +23 -0
- package/dist/core/lb-constants.d.ts.map +1 -0
- package/dist/core/lb-constants.js +95 -0
- package/dist/core/lb-types.d.ts +88 -0
- package/dist/core/lb-types.d.ts.map +1 -0
- package/dist/core/lb-types.js +5 -0
- package/dist/demo-static/src/widgets/app-box.d.ts +15 -0
- package/dist/demo-static/src/widgets/app-box.d.ts.map +1 -0
- package/dist/demo-static/src/widgets/app-box.js +19 -0
- package/dist/hub/lb-apply.d.ts +13 -0
- package/dist/hub/lb-apply.d.ts.map +1 -0
- package/dist/hub/lb-apply.js +77 -0
- package/dist/hub/lb-hub.d.ts +2 -0
- package/dist/hub/lb-hub.d.ts.map +1 -0
- package/dist/hub/lb-hub.js +242 -0
- package/dist/hub/lb-rows.d.ts +18 -0
- package/dist/hub/lb-rows.d.ts.map +1 -0
- package/dist/hub/lb-rows.js +106 -0
- package/dist/server/lb-express.d.ts +28 -0
- package/dist/server/lb-express.d.ts.map +1 -0
- package/dist/server/lb-express.js +77 -0
- package/dist/server/lb-server.d.ts +174 -0
- package/dist/server/lb-server.d.ts.map +1 -0
- package/dist/server/lb-server.js +79 -0
- package/dist/tests/assemble.test.d.ts +8 -0
- package/dist/tests/assemble.test.d.ts.map +1 -0
- package/dist/tests/assemble.test.js +51 -0
- package/dist/tests/elements.test.d.ts +8 -0
- package/dist/tests/elements.test.d.ts.map +1 -0
- package/dist/tests/elements.test.js +111 -0
- package/dist/tests/expand.test.d.ts +10 -0
- package/dist/tests/expand.test.d.ts.map +1 -0
- package/dist/tests/expand.test.js +226 -0
- package/dist/tests/fixtures/elements/collision/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/collision/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/elements.js +3 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts +2 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/collision/widgets/acme-widget.js +1 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts +2 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/local/widgets/app-box.js +1 -0
- package/dist/tests/fixtures/elements/manifest/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest/elements.js +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-tag/elements.js +3 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts +5 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.d.ts.map +1 -0
- package/dist/tests/fixtures/elements/manifest-bad-value/elements.js +3 -0
- package/dist/tests/golden.test.d.ts +19 -0
- package/dist/tests/golden.test.d.ts.map +1 -0
- package/dist/tests/golden.test.js +60 -0
- package/dist/tests/helpers/console.d.ts +20 -0
- package/dist/tests/helpers/console.d.ts.map +1 -0
- package/dist/tests/helpers/console.js +28 -0
- package/dist/tests/helpers/dom.d.ts +18 -0
- package/dist/tests/helpers/dom.d.ts.map +1 -0
- package/dist/tests/helpers/dom.js +22 -0
- package/dist/tests/helpers/window.d.ts +43 -0
- package/dist/tests/helpers/window.d.ts.map +1 -0
- package/dist/tests/helpers/window.js +78 -0
- package/dist/tests/lb-apply.test.d.ts +8 -0
- package/dist/tests/lb-apply.test.d.ts.map +1 -0
- package/dist/tests/lb-apply.test.js +153 -0
- package/dist/tests/lb-express.test.d.ts +14 -0
- package/dist/tests/lb-express.test.d.ts.map +1 -0
- package/dist/tests/lb-express.test.js +238 -0
- package/dist/tests/lb-input.test.d.ts +9 -0
- package/dist/tests/lb-input.test.d.ts.map +1 -0
- package/dist/tests/lb-input.test.js +78 -0
- package/dist/tests/lb-list.test.d.ts +12 -0
- package/dist/tests/lb-list.test.d.ts.map +1 -0
- package/dist/tests/lb-list.test.js +44 -0
- package/dist/tests/lb-options.test.d.ts +10 -0
- package/dist/tests/lb-options.test.d.ts.map +1 -0
- package/dist/tests/lb-options.test.js +121 -0
- package/dist/tests/lb-picker.test.d.ts +14 -0
- package/dist/tests/lb-picker.test.d.ts.map +1 -0
- package/dist/tests/lb-picker.test.js +59 -0
- package/dist/tests/lb-rows.test.d.ts +12 -0
- package/dist/tests/lb-rows.test.d.ts.map +1 -0
- package/dist/tests/lb-rows.test.js +336 -0
- package/dist/tests/lb-select.test.d.ts +9 -0
- package/dist/tests/lb-select.test.d.ts.map +1 -0
- package/dist/tests/lb-select.test.js +71 -0
- package/dist/tests/lb-server.test.d.ts +9 -0
- package/dist/tests/lb-server.test.d.ts.map +1 -0
- package/dist/tests/lb-server.test.js +495 -0
- package/dist/tests/lb-table.test.d.ts +15 -0
- package/dist/tests/lb-table.test.d.ts.map +1 -0
- package/dist/tests/lb-table.test.js +205 -0
- package/dist/tests/pages.test.d.ts +6 -0
- package/dist/tests/pages.test.d.ts.map +1 -0
- package/dist/tests/pages.test.js +98 -0
- package/dist/tests/styles.test.d.ts +7 -0
- package/dist/tests/styles.test.d.ts.map +1 -0
- package/dist/tests/styles.test.js +73 -0
- package/dist/widgets/index.d.ts +7 -0
- package/dist/widgets/index.d.ts.map +1 -0
- package/dist/widgets/index.js +6 -0
- package/dist/widgets/lb-input.d.ts +2 -0
- package/dist/widgets/lb-input.d.ts.map +1 -0
- package/dist/widgets/lb-input.js +48 -0
- package/dist/widgets/lb-list.d.ts +2 -0
- package/dist/widgets/lb-list.d.ts.map +1 -0
- package/dist/widgets/lb-list.js +17 -0
- package/dist/widgets/lb-options.d.ts +26 -0
- package/dist/widgets/lb-options.d.ts.map +1 -0
- package/dist/widgets/lb-options.js +72 -0
- package/dist/widgets/lb-picker.d.ts +2 -0
- package/dist/widgets/lb-picker.d.ts.map +1 -0
- package/dist/widgets/lb-picker.js +25 -0
- package/dist/widgets/lb-select.d.ts +2 -0
- package/dist/widgets/lb-select.d.ts.map +1 -0
- package/dist/widgets/lb-select.js +43 -0
- package/dist/widgets/lb-table.d.ts +2 -0
- package/dist/widgets/lb-table.d.ts.map +1 -0
- package/dist/widgets/lb-table.js +113 -0
- package/docs/application-chrome.md +36 -0
- package/docs/building-html-pages.md +130 -0
- package/docs/getting-started.md +120 -0
- package/docs/guide.md +1164 -0
- package/docs/hosting.md +218 -0
- package/docs/latent-risks.md +20 -0
- package/docs/theory.md +226 -0
- package/package.json +85 -0
- package/widgets/index.ts +6 -0
- package/widgets/lb-input.html +1 -0
- package/widgets/lb-input.ts +64 -0
- package/widgets/lb-list.html +1 -0
- package/widgets/lb-list.ts +21 -0
- package/widgets/lb-options.html +4 -0
- package/widgets/lb-options.ts +88 -0
- package/widgets/lb-picker.html +7 -0
- package/widgets/lb-picker.ts +27 -0
- package/widgets/lb-select.html +4 -0
- package/widgets/lb-select.ts +55 -0
- package/widgets/lb-table.html +8 -0
- package/widgets/lb-table.ts +126 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// core/lb-constants.ts
|
|
4
|
+
var ATTR_QUERY = "lb-query";
|
|
5
|
+
var ATTR_KEY = "lb-key";
|
|
6
|
+
var ATTR_CELL = "lb-cell";
|
|
7
|
+
var ATTR_VALUE = "lb-value";
|
|
8
|
+
var ATTR_GROUP = "lb-group";
|
|
9
|
+
var ATTR_SORT = "lb-sort";
|
|
10
|
+
var ATTR_ROW_COUNT = "data-rows";
|
|
11
|
+
var LB_EVENT_NAME = "lb-request";
|
|
12
|
+
var ATTR_ACTION = "lb-action";
|
|
13
|
+
var ATTR_DELETE = "lb-delete";
|
|
14
|
+
var ATTR_INSERT = "lb-insert";
|
|
15
|
+
var ATTR_UPDATE = "lb-update";
|
|
16
|
+
var ATTR_NAV_LINK = "lb-nav-link";
|
|
17
|
+
var PAGE_TEMPLATE_PREFIX = "page-";
|
|
18
|
+
var HUB_TAG_NAME = "lb-hub";
|
|
19
|
+
var ATTR_PENDING = "data-lb-pending";
|
|
20
|
+
var ATTR_ERROR = "data-lb-error";
|
|
21
|
+
var LB_DATA_ENDPOINT = "/lb/data";
|
|
22
|
+
var LB_REQUEST_ENDPOINT = "/lb";
|
|
23
|
+
var LB_REQUEST_TIMEOUT_MS = 1e4;
|
|
24
|
+
|
|
25
|
+
// widgets/lb-input.ts
|
|
26
|
+
var LbInput = class extends HTMLElement {
|
|
27
|
+
static observedAttributes = [ATTR_VALUE];
|
|
28
|
+
attributeChangedCallback(_name, _old, value) {
|
|
29
|
+
const input = this.querySelector("input");
|
|
30
|
+
if (!input) {
|
|
31
|
+
console.error(`lb-input: no <input> to receive the value`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
input.value = value;
|
|
35
|
+
}
|
|
36
|
+
connectedCallback() {
|
|
37
|
+
this.addEventListener("change", () => {
|
|
38
|
+
const input = this.querySelector("input");
|
|
39
|
+
if (!input) {
|
|
40
|
+
console.error(`lb-input: change with no <input>, ignoring`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const query = this.closest(`[${ATTR_QUERY}]`)?.getAttribute(ATTR_QUERY);
|
|
44
|
+
const key = this.closest(`[${ATTR_KEY}]`)?.getAttribute(ATTR_KEY);
|
|
45
|
+
const cell = this.getAttribute(ATTR_CELL);
|
|
46
|
+
if (!query || !key || !cell) {
|
|
47
|
+
console.error(
|
|
48
|
+
`lb-input: change with no query/key/cell coordinates, ignoring`
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const detail = {
|
|
53
|
+
op: "cell-change",
|
|
54
|
+
query,
|
|
55
|
+
key,
|
|
56
|
+
cell,
|
|
57
|
+
value: input.value
|
|
58
|
+
};
|
|
59
|
+
this.dispatchEvent(
|
|
60
|
+
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
customElements.define("lb-input", LbInput);
|
|
66
|
+
|
|
67
|
+
// widgets/lb-select.ts
|
|
68
|
+
var LbSelect = class extends HTMLElement {
|
|
69
|
+
static observedAttributes = [ATTR_VALUE];
|
|
70
|
+
attributeChangedCallback(_name, _old, value) {
|
|
71
|
+
const select = this.querySelector("select");
|
|
72
|
+
if (!select) {
|
|
73
|
+
console.error(`lb-select: no <select> to receive the value`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
select.value = value;
|
|
77
|
+
}
|
|
78
|
+
connectedCallback() {
|
|
79
|
+
this.addEventListener("change", () => {
|
|
80
|
+
const select = this.querySelector("select");
|
|
81
|
+
if (!select) {
|
|
82
|
+
console.error(`lb-select: change with no <select>, ignoring`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const name = this.getAttribute(ATTR_ACTION);
|
|
86
|
+
if (!name) {
|
|
87
|
+
console.error(`lb-select: no ${ATTR_ACTION}, nothing to send`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const detail = {
|
|
91
|
+
op: "action",
|
|
92
|
+
name,
|
|
93
|
+
value: select.value
|
|
94
|
+
};
|
|
95
|
+
this.dispatchEvent(
|
|
96
|
+
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
customElements.define("lb-select", LbSelect);
|
|
102
|
+
|
|
103
|
+
// core/lb-types.ts
|
|
104
|
+
function isProjection(result) {
|
|
105
|
+
const op = result.op;
|
|
106
|
+
return op === "rows" || op === "patch";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// hub/lb-apply.ts
|
|
110
|
+
function land(el, value) {
|
|
111
|
+
if (el.localName.includes("-")) el.setAttribute(ATTR_VALUE, value);
|
|
112
|
+
else el.textContent = value;
|
|
113
|
+
}
|
|
114
|
+
function applyTuple(root, cells) {
|
|
115
|
+
for (const [cell, value] of Object.entries(cells)) {
|
|
116
|
+
const selector = `[${ATTR_CELL}="${cell}"]`;
|
|
117
|
+
if (root.matches(selector)) land(root, value);
|
|
118
|
+
for (const el of root.querySelectorAll(selector)) land(el, value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function landRows(scope, query, result) {
|
|
122
|
+
const host = scope;
|
|
123
|
+
if (typeof host.acceptRows !== "function") {
|
|
124
|
+
console.error(
|
|
125
|
+
`lb-hub: query '${query}' returned rows, but <${scope.localName}> is not a list widget`
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
host.acceptRows(result);
|
|
130
|
+
}
|
|
131
|
+
function applyData(root, data) {
|
|
132
|
+
for (const [query, result] of Object.entries(data)) {
|
|
133
|
+
const scopes = root.querySelectorAll(`[${ATTR_QUERY}="${query}"]`);
|
|
134
|
+
if (scopes.length === 0) {
|
|
135
|
+
console.warn(`lb-hub: no scope for query '${query}', skipping`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (const scope of scopes) {
|
|
139
|
+
if (isProjection(result)) landRows(scope, query, result);
|
|
140
|
+
else applyTuple(scope, result);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// hub/lb-rows.ts
|
|
146
|
+
var before = (row, _tuple, template) => {
|
|
147
|
+
template.parentElement.insertBefore(row, template);
|
|
148
|
+
};
|
|
149
|
+
function templateIn(scope) {
|
|
150
|
+
const template = scope.querySelector("template");
|
|
151
|
+
if (!template) {
|
|
152
|
+
console.error(
|
|
153
|
+
`lb-rows: <${scope.localName}> has no <template> to clone`
|
|
154
|
+
);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (!template.getAttribute(ATTR_KEY)) {
|
|
158
|
+
console.error(
|
|
159
|
+
`lb-rows: the <template> in <${scope.localName}> has no ${ATTR_KEY} naming the column that identifies a row`
|
|
160
|
+
);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return template;
|
|
164
|
+
}
|
|
165
|
+
function showing(scope, template) {
|
|
166
|
+
const rows = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const el of scope.querySelectorAll(`[${ATTR_KEY}]`)) {
|
|
168
|
+
if (el !== template) rows.set(el.getAttribute(ATTR_KEY), el);
|
|
169
|
+
}
|
|
170
|
+
return rows;
|
|
171
|
+
}
|
|
172
|
+
function applyRows(scope, result, place = before) {
|
|
173
|
+
const template = templateIn(scope);
|
|
174
|
+
if (!template) return;
|
|
175
|
+
const keyCell = template.getAttribute(ATTR_KEY);
|
|
176
|
+
const rows = showing(scope, template);
|
|
177
|
+
const upsert = (tuple) => {
|
|
178
|
+
const key = tuple[keyCell];
|
|
179
|
+
if (key === void 0) {
|
|
180
|
+
console.error(
|
|
181
|
+
`lb-rows: a tuple for <${scope.localName}> has no '${keyCell}' cell`
|
|
182
|
+
);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
let row = rows.get(key);
|
|
186
|
+
const fresh = row === void 0;
|
|
187
|
+
if (!row) {
|
|
188
|
+
row = template.content.firstElementChild.cloneNode(true);
|
|
189
|
+
row.setAttribute(ATTR_KEY, key);
|
|
190
|
+
rows.set(key, row);
|
|
191
|
+
}
|
|
192
|
+
applyTuple(row, tuple);
|
|
193
|
+
if (fresh || result.op === "rows") place(row, tuple, template);
|
|
194
|
+
return key;
|
|
195
|
+
};
|
|
196
|
+
if (result.op === "rows") {
|
|
197
|
+
const arrived = /* @__PURE__ */ new Set();
|
|
198
|
+
for (const tuple of result.rows) {
|
|
199
|
+
const key = upsert(tuple);
|
|
200
|
+
if (key !== null) arrived.add(key);
|
|
201
|
+
}
|
|
202
|
+
for (const [key, row] of rows) if (!arrived.has(key)) row.remove();
|
|
203
|
+
} else {
|
|
204
|
+
for (const tuple of result.rows ?? []) upsert(tuple);
|
|
205
|
+
for (const key of result.drop ?? []) rows.get(key)?.remove();
|
|
206
|
+
}
|
|
207
|
+
scope.setAttribute(ATTR_ROW_COUNT, String(showing(scope, template).size));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// widgets/lb-list.ts
|
|
211
|
+
var LbList = class extends HTMLElement {
|
|
212
|
+
acceptRows(result) {
|
|
213
|
+
applyRows(this, result);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
customElements.define("lb-list", LbList);
|
|
217
|
+
|
|
218
|
+
// widgets/lb-options.ts
|
|
219
|
+
var LbOptions = class extends HTMLElement {
|
|
220
|
+
acceptRows(result) {
|
|
221
|
+
applyRows(this, result, this.place);
|
|
222
|
+
for (const option of this.querySelectorAll(`option[${ATTR_KEY}]`)) {
|
|
223
|
+
option.setAttribute("value", option.getAttribute(ATTR_KEY));
|
|
224
|
+
}
|
|
225
|
+
for (const group of this.querySelectorAll("optgroup")) {
|
|
226
|
+
if (!group.querySelector("option")) group.remove();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
place = (row, tuple, template) => {
|
|
230
|
+
const select = this.querySelector("select");
|
|
231
|
+
const groupCell = template.getAttribute(ATTR_GROUP);
|
|
232
|
+
if (!groupCell) {
|
|
233
|
+
select.insertBefore(row, template);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
this.groupFor(select, template, tuple[groupCell] ?? "").append(row);
|
|
237
|
+
};
|
|
238
|
+
/** The group with this label, or a new one in front of the template. */
|
|
239
|
+
groupFor(select, template, label) {
|
|
240
|
+
for (const group2 of select.querySelectorAll("optgroup")) {
|
|
241
|
+
if (group2.label === label) return group2;
|
|
242
|
+
}
|
|
243
|
+
const group = document.createElement("optgroup");
|
|
244
|
+
group.label = label;
|
|
245
|
+
select.insertBefore(group, template);
|
|
246
|
+
return group;
|
|
247
|
+
}
|
|
248
|
+
connectedCallback() {
|
|
249
|
+
this.addEventListener("change", () => {
|
|
250
|
+
const select = this.querySelector("select");
|
|
251
|
+
if (!select) {
|
|
252
|
+
console.error(`lb-options: change with no <select>, ignoring`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const name = this.getAttribute(ATTR_ACTION);
|
|
256
|
+
if (!name) {
|
|
257
|
+
console.error(`lb-options: no ${ATTR_ACTION}, nothing to send`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const detail = { op: "action", name, value: select.value };
|
|
261
|
+
this.dispatchEvent(
|
|
262
|
+
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
customElements.define("lb-options", LbOptions);
|
|
268
|
+
|
|
269
|
+
// widgets/lb-table.ts
|
|
270
|
+
var GROUP_ROW = "data-group";
|
|
271
|
+
var LbTable = class extends HTMLElement {
|
|
272
|
+
acceptRows(result) {
|
|
273
|
+
applyRows(this, result, this.place);
|
|
274
|
+
for (const heading of this.querySelectorAll(`tr[${GROUP_ROW}]`)) {
|
|
275
|
+
if (this.section(heading).rows.length === 0) heading.remove();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Group first, then sort within the group.
|
|
280
|
+
*
|
|
281
|
+
* Inserting a row before the first one that sorts after it keeps the
|
|
282
|
+
* section ordered, whatever else is in it — which is what lets a whole set
|
|
283
|
+
* and a single patched row take the same path.
|
|
284
|
+
*/
|
|
285
|
+
place = (row, tuple, template) => {
|
|
286
|
+
const groupCell = template.getAttribute(ATTR_GROUP);
|
|
287
|
+
const sortCell = template.getAttribute(ATTR_SORT);
|
|
288
|
+
const heading = groupCell ? this.headingFor(template, tuple[groupCell] ?? "") : null;
|
|
289
|
+
const { rows, end } = this.section(heading);
|
|
290
|
+
const value = sortCell ? tuple[sortCell] ?? "" : "";
|
|
291
|
+
const after = sortCell ? rows.find((other) => other !== row && cellOf(other, sortCell) > value) : void 0;
|
|
292
|
+
template.parentElement.insertBefore(row, after ?? end);
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* The rows of one section, and the node it ends at.
|
|
296
|
+
*
|
|
297
|
+
* A section runs from its heading to the next heading, or to the template
|
|
298
|
+
* if there is none after it. Without grouping there is one section and it
|
|
299
|
+
* is the whole body — the heading row the page wrote is skipped along the
|
|
300
|
+
* way, because it carries no key and is therefore not a row.
|
|
301
|
+
*/
|
|
302
|
+
section(heading) {
|
|
303
|
+
const template = this.querySelector("template");
|
|
304
|
+
const rows = [];
|
|
305
|
+
let node = heading ? heading.nextElementSibling : template.parentElement.firstElementChild;
|
|
306
|
+
while (node && node !== template && !node.hasAttribute(GROUP_ROW)) {
|
|
307
|
+
if (node.hasAttribute(ATTR_KEY)) rows.push(node);
|
|
308
|
+
node = node.nextElementSibling;
|
|
309
|
+
}
|
|
310
|
+
return { rows, end: node ?? template };
|
|
311
|
+
}
|
|
312
|
+
/** The section with this label, or a new one in front of the template. */
|
|
313
|
+
headingFor(template, label) {
|
|
314
|
+
const body = template.parentElement;
|
|
315
|
+
for (const heading2 of body.querySelectorAll(`tr[${GROUP_ROW}]`)) {
|
|
316
|
+
if (heading2.getAttribute(GROUP_ROW) === label) return heading2;
|
|
317
|
+
}
|
|
318
|
+
const heading = document.createElement("tr");
|
|
319
|
+
heading.setAttribute(GROUP_ROW, label);
|
|
320
|
+
const cell = document.createElement("th");
|
|
321
|
+
cell.setAttribute("scope", "rowgroup");
|
|
322
|
+
cell.colSpan = template.content.firstElementChild?.childElementCount ?? 1;
|
|
323
|
+
cell.textContent = label;
|
|
324
|
+
heading.append(cell);
|
|
325
|
+
body.insertBefore(heading, template);
|
|
326
|
+
return heading;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
function cellOf(row, cell) {
|
|
330
|
+
const selector = `[${ATTR_CELL}="${cell}"]`;
|
|
331
|
+
const el = row.matches(selector) ? row : row.querySelector(selector);
|
|
332
|
+
return el?.textContent ?? "";
|
|
333
|
+
}
|
|
334
|
+
customElements.define("lb-table", LbTable);
|
|
335
|
+
|
|
336
|
+
// widgets/lb-picker.ts
|
|
337
|
+
var LbPicker = class extends LbOptions {
|
|
338
|
+
};
|
|
339
|
+
customElements.define("lb-picker", LbPicker);
|
|
340
|
+
|
|
341
|
+
// hub/lb-hub.ts
|
|
342
|
+
function coordinates(el) {
|
|
343
|
+
const query = el.closest(`[${ATTR_QUERY}]`)?.getAttribute(ATTR_QUERY);
|
|
344
|
+
const key = el.closest(`[${ATTR_KEY}]`)?.getAttribute(ATTR_KEY);
|
|
345
|
+
const cell = el.getAttribute(ATTR_CELL);
|
|
346
|
+
return {
|
|
347
|
+
...query === null || query === void 0 ? {} : { query },
|
|
348
|
+
...key === null || key === void 0 ? {} : { key },
|
|
349
|
+
...cell === null ? {} : { cell }
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function controlValue(el) {
|
|
353
|
+
const control = el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement ? el : el.querySelector("input, select, textarea");
|
|
354
|
+
return control?.value;
|
|
355
|
+
}
|
|
356
|
+
function gatherValues(form) {
|
|
357
|
+
const values = {};
|
|
358
|
+
for (const cell of form.querySelectorAll(`[${ATTR_CELL}]`)) {
|
|
359
|
+
const name = cell.getAttribute(ATTR_CELL);
|
|
360
|
+
const value = controlValue(cell);
|
|
361
|
+
if (value === void 0) {
|
|
362
|
+
console.warn(
|
|
363
|
+
`lb-hub: lb-cell '${name}' has no control to read, ignoring it`,
|
|
364
|
+
cell
|
|
365
|
+
);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
values[name] = value;
|
|
369
|
+
}
|
|
370
|
+
return values;
|
|
371
|
+
}
|
|
372
|
+
function pageNameFor(path) {
|
|
373
|
+
const name = path.replace(/^\/+|\/+$/g, "");
|
|
374
|
+
return name === "" ? "counter" : name;
|
|
375
|
+
}
|
|
376
|
+
async function fetchData(url, init) {
|
|
377
|
+
const controller = new AbortController();
|
|
378
|
+
const timeout = setTimeout(() => controller.abort(), LB_REQUEST_TIMEOUT_MS);
|
|
379
|
+
try {
|
|
380
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
381
|
+
if (!res.ok) {
|
|
382
|
+
throw new Error(`lb-hub: server responded ${res.status} for ${url}`);
|
|
383
|
+
}
|
|
384
|
+
return await res.json();
|
|
385
|
+
} finally {
|
|
386
|
+
clearTimeout(timeout);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
var LbHub = class extends HTMLElement {
|
|
390
|
+
main;
|
|
391
|
+
/**
|
|
392
|
+
* The page currently in <main>. Hooks are declared per page, so a request
|
|
393
|
+
* has to say which page it came from. The page rides on the URL rather
|
|
394
|
+
* than in the request, leaving the operation set closed.
|
|
395
|
+
*/
|
|
396
|
+
page = "";
|
|
397
|
+
connectedCallback() {
|
|
398
|
+
this.main = this.querySelector("main");
|
|
399
|
+
this.addEventListener(LB_EVENT_NAME, (e) => {
|
|
400
|
+
const request = e.detail;
|
|
401
|
+
if (!request?.op) {
|
|
402
|
+
console.warn(`lb-hub: event without an op, ignoring`, request);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const origin = e.target;
|
|
406
|
+
origin?.removeAttribute(ATTR_ERROR);
|
|
407
|
+
origin?.setAttribute(ATTR_PENDING, "");
|
|
408
|
+
const url = `${LB_REQUEST_ENDPOINT}?page=${encodeURIComponent(this.page)}`;
|
|
409
|
+
void fetchData(url, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
headers: { "content-type": "application/json" },
|
|
412
|
+
body: JSON.stringify(request)
|
|
413
|
+
}).then((data) => applyData(this.main, data)).catch((err) => {
|
|
414
|
+
console.error(`lb-hub: request failed`, err);
|
|
415
|
+
origin?.setAttribute(ATTR_ERROR, "");
|
|
416
|
+
}).finally(() => origin?.removeAttribute(ATTR_PENDING));
|
|
417
|
+
});
|
|
418
|
+
this.addEventListener("click", (e) => {
|
|
419
|
+
const el = e.target?.closest(`[${ATTR_ACTION}]`);
|
|
420
|
+
if (!el || el.localName.includes("-")) return;
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
const detail = {
|
|
423
|
+
op: "action",
|
|
424
|
+
name: el.getAttribute(ATTR_ACTION),
|
|
425
|
+
...coordinates(el)
|
|
426
|
+
};
|
|
427
|
+
el.dispatchEvent(
|
|
428
|
+
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
this.addEventListener("click", (e) => {
|
|
432
|
+
const el = e.target?.closest(`[${ATTR_DELETE}]`);
|
|
433
|
+
if (!el || el.localName.includes("-")) return;
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
const { query, key } = coordinates(el);
|
|
436
|
+
if (!query || !key) {
|
|
437
|
+
console.warn(`lb-hub: lb-delete with no query/key coordinates, ignoring`, el);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const detail = { op: "tuple-delete", query, key };
|
|
441
|
+
el.dispatchEvent(
|
|
442
|
+
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
this.addEventListener("submit", (e) => {
|
|
446
|
+
const form = e.target?.closest(`[${ATTR_INSERT}]`);
|
|
447
|
+
if (!form) return;
|
|
448
|
+
e.preventDefault();
|
|
449
|
+
const { query } = coordinates(form);
|
|
450
|
+
if (!query) {
|
|
451
|
+
console.warn(`lb-hub: lb-insert with no lb-query, ignoring`, form);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const detail = {
|
|
455
|
+
op: "tuple-insert",
|
|
456
|
+
query,
|
|
457
|
+
values: gatherValues(form)
|
|
458
|
+
};
|
|
459
|
+
form.dispatchEvent(
|
|
460
|
+
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
this.addEventListener("submit", (e) => {
|
|
464
|
+
const form = e.target?.closest(`[${ATTR_UPDATE}]`);
|
|
465
|
+
if (!form) return;
|
|
466
|
+
e.preventDefault();
|
|
467
|
+
const { query, key } = coordinates(form);
|
|
468
|
+
if (!query || !key) {
|
|
469
|
+
console.warn(
|
|
470
|
+
`lb-hub: lb-update with no query/key coordinates, ignoring`,
|
|
471
|
+
form
|
|
472
|
+
);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const detail = {
|
|
476
|
+
op: "tuple-update",
|
|
477
|
+
query,
|
|
478
|
+
key,
|
|
479
|
+
values: gatherValues(form)
|
|
480
|
+
};
|
|
481
|
+
form.dispatchEvent(
|
|
482
|
+
new CustomEvent(LB_EVENT_NAME, { bubbles: true, detail })
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
this.addEventListener("click", (e) => {
|
|
486
|
+
const link = e.target?.closest(`a[${ATTR_NAV_LINK}]`);
|
|
487
|
+
if (!link) return;
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
const path = new URL(link.href).pathname;
|
|
490
|
+
history.pushState(null, "", path);
|
|
491
|
+
void this.navigate(path);
|
|
492
|
+
});
|
|
493
|
+
addEventListener("popstate", () => void this.navigate(location.pathname));
|
|
494
|
+
void this.navigate(location.pathname);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Navigation is the one operation that replaces host DOM. The host is
|
|
498
|
+
* already in the document, so selecting a page is getElementById() and
|
|
499
|
+
* there is no fetching mechanism on the host channel.
|
|
500
|
+
*/
|
|
501
|
+
async navigate(path) {
|
|
502
|
+
const page = pageNameFor(path);
|
|
503
|
+
const template = document.getElementById(PAGE_TEMPLATE_PREFIX + page);
|
|
504
|
+
if (!(template instanceof HTMLTemplateElement)) {
|
|
505
|
+
console.error(`lb-hub: no page host for '${page}'`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
let data = {};
|
|
509
|
+
try {
|
|
510
|
+
data = await fetchData(
|
|
511
|
+
`${LB_DATA_ENDPOINT}?page=${encodeURIComponent(page)}`
|
|
512
|
+
);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
console.error(`lb-hub: failed to load data for '${page}'`, err);
|
|
515
|
+
}
|
|
516
|
+
this.page = page;
|
|
517
|
+
this.main.replaceChildren(template.content.cloneNode(true));
|
|
518
|
+
applyData(this.main, data);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
customElements.define(HUB_TAG_NAME, LbHub);
|
|
522
|
+
})();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const ATTR_QUERY = "lb-query";
|
|
2
|
+
export declare const ATTR_KEY = "lb-key";
|
|
3
|
+
export declare const ATTR_CELL = "lb-cell";
|
|
4
|
+
export declare const ATTR_VALUE = "lb-value";
|
|
5
|
+
export declare const ATTR_GROUP = "lb-group";
|
|
6
|
+
export declare const ATTR_SORT = "lb-sort";
|
|
7
|
+
export declare const ATTR_ROW_COUNT = "data-rows";
|
|
8
|
+
export declare const LB_EVENT_NAME = "lb-request";
|
|
9
|
+
export declare const ATTR_ACTION = "lb-action";
|
|
10
|
+
export declare const ATTR_DELETE = "lb-delete";
|
|
11
|
+
export declare const ATTR_INSERT = "lb-insert";
|
|
12
|
+
export declare const ATTR_UPDATE = "lb-update";
|
|
13
|
+
export declare const ATTR_SLOT = "lb-slot";
|
|
14
|
+
export declare const ATTR_TEMPLATE = "lb-template";
|
|
15
|
+
export declare const ATTR_NAV_LINK = "lb-nav-link";
|
|
16
|
+
export declare const PAGE_TEMPLATE_PREFIX = "page-";
|
|
17
|
+
export declare const HUB_TAG_NAME = "lb-hub";
|
|
18
|
+
export declare const ATTR_PENDING = "data-lb-pending";
|
|
19
|
+
export declare const ATTR_ERROR = "data-lb-error";
|
|
20
|
+
export declare const LB_DATA_ENDPOINT = "/lb/data";
|
|
21
|
+
export declare const LB_REQUEST_ENDPOINT = "/lb";
|
|
22
|
+
export declare const LB_REQUEST_TIMEOUT_MS = 10000;
|
|
23
|
+
//# sourceMappingURL=lb-constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lb-constants.d.ts","sourceRoot":"","sources":["../../core/lb-constants.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,UAAU,aAAa,CAAC;AACrC,eAAO,MAAM,QAAQ,WAAW,CAAC;AACjC,eAAO,MAAM,SAAS,YAAY,CAAC;AACnC,eAAO,MAAM,UAAU,aAAa,CAAC;AAMrC,eAAO,MAAM,UAAU,aAAa,CAAC;AAKrC,eAAO,MAAM,SAAS,YAAY,CAAC;AAOnC,eAAO,MAAM,cAAc,cAAc,CAAC;AAK1C,eAAO,MAAM,aAAa,eAAe,CAAC;AAM1C,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAKvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAOvC,eAAO,MAAM,WAAW,cAAc,CAAC;AAMvC,eAAO,MAAM,SAAS,YAAY,CAAC;AAYnC,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAG3C,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAQ5C,eAAO,MAAM,YAAY,WAAW,CAAC;AAQrC,eAAO,MAAM,YAAY,oBAAoB,CAAC;AAO9C,eAAO,MAAM,UAAU,kBAAkB,CAAC;AAG1C,eAAO,MAAM,gBAAgB,aAAa,CAAC;AAC3C,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAIzC,eAAO,MAAM,qBAAqB,QAAS,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Every hub attribute name lives here. Nothing writes one as a string
|
|
2
|
+
// literal — not a widget, not a page host, not a test. See docs/guide.md,
|
|
3
|
+
// "Attribute vocabulary".
|
|
4
|
+
// --- Addressing (docs/data-binding.md) ---------------------------------
|
|
5
|
+
export const ATTR_QUERY = "lb-query";
|
|
6
|
+
export const ATTR_KEY = "lb-key";
|
|
7
|
+
export const ATTR_CELL = "lb-cell";
|
|
8
|
+
export const ATTR_VALUE = "lb-value";
|
|
9
|
+
// Named on a row template, and read by a list widget that groups its rows
|
|
10
|
+
// rather than by the hub. Grouping is placement, and placement is the
|
|
11
|
+
// widget's — but the name is shared, because a grouped <select> and a table
|
|
12
|
+
// with section headings are asking the same thing of the same data.
|
|
13
|
+
export const ATTR_GROUP = "lb-group";
|
|
14
|
+
// Named on a row template beside lb-group, and read by the same kind of
|
|
15
|
+
// widget for the same reason. Sorting and grouping are one question — where
|
|
16
|
+
// does this row go? — and both answers are local.
|
|
17
|
+
export const ATTR_SORT = "lb-sort";
|
|
18
|
+
// Stamped on a list widget by the shared row machinery: how many rows it is
|
|
19
|
+
// showing. It is HTML's namespace rather than Loadbare App's because nothing
|
|
20
|
+
// addresses it — it is derived from rows the hub already knows, exactly as
|
|
21
|
+
// the section heading in lb-table is. It exists so that "no members yet"
|
|
22
|
+
// is a stylesheet rule rather than a conditional in every list widget.
|
|
23
|
+
export const ATTR_ROW_COUNT = "data-rows";
|
|
24
|
+
// --- Requests (docs/requests.md) ---------------------------------------
|
|
25
|
+
// One event name, used by a widget reaching the server and by a widget
|
|
26
|
+
// reaching an ancestor widget.
|
|
27
|
+
export const LB_EVENT_NAME = "lb-request";
|
|
28
|
+
// Names a declared action. On a native element, the hub turns a click into
|
|
29
|
+
// the request; on a widget, the widget builds it from whatever interaction
|
|
30
|
+
// it owns. Either way the name must appear in the page's hooks or it is
|
|
31
|
+
// refused.
|
|
32
|
+
export const ATTR_ACTION = "lb-action";
|
|
33
|
+
// Marks a native element as deleting the tuple it is inside — the CRUD
|
|
34
|
+
// counterpart to lb-action. It needs no declared name: tuple-delete takes
|
|
35
|
+
// only the row's own lb-query/lb-key coordinates, which is all the server
|
|
36
|
+
// needs to know which row and whether the operation is permitted on it.
|
|
37
|
+
export const ATTR_DELETE = "lb-delete";
|
|
38
|
+
// Marks a <form> as building a new tuple on submit. The hub gathers every
|
|
39
|
+
// lb-cell inside it into a values map — no lb-key, because there is no row
|
|
40
|
+
// yet — and the form's own lb-query says which query it is inserting into.
|
|
41
|
+
export const ATTR_INSERT = "lb-insert";
|
|
42
|
+
// Marks a <form> as committing several cells of one existing tuple at once
|
|
43
|
+
// on submit — the batch counterpart to cell-change. The hub gathers the
|
|
44
|
+
// same values map lb-insert does, plus the row's own lb-key: a form that is
|
|
45
|
+
// a list's row template root, or sits inside one, has both from its
|
|
46
|
+
// ancestors exactly as a delete button does.
|
|
47
|
+
export const ATTR_UPDATE = "lb-update";
|
|
48
|
+
// --- Expansion (docs/expansion.md) -------------------------------------
|
|
49
|
+
// Marks the element in a definition whose children receive the authored
|
|
50
|
+
// inner content. An attribute rather than an element because HTML content
|
|
51
|
+
// models discard foreign elements inside <select> and <table>.
|
|
52
|
+
export const ATTR_SLOT = "lb-slot";
|
|
53
|
+
// One name in two positions. In a definition it marks a destination; on an
|
|
54
|
+
// authored <template> it names the destination that template is for, and the
|
|
55
|
+
// template is unwrapped into it.
|
|
56
|
+
//
|
|
57
|
+
// It exists because the parser, not the content model, is the obstacle. A
|
|
58
|
+
// <thead> written inside <lb-table> on a page is not in a table, so the
|
|
59
|
+
// tokenizer drops the tag and keeps the text. A <template> survives anywhere
|
|
60
|
+
// and its contents parse as if they were in a table, so wrapping the markup
|
|
61
|
+
// is what gets it to expansion intact — and unwrapping it is what puts real
|
|
62
|
+
// elements in the shipped document.
|
|
63
|
+
export const ATTR_TEMPLATE = "lb-template";
|
|
64
|
+
// --- Host channel (docs/bundling.md) -----------------------------------
|
|
65
|
+
// Only anchors carrying this are intercepted, so external links are never
|
|
66
|
+
// hijacked.
|
|
67
|
+
export const ATTR_NAV_LINK = "lb-nav-link";
|
|
68
|
+
// A page host ships as <template id="page-<name>">.
|
|
69
|
+
export const PAGE_TEMPLATE_PREFIX = "page-";
|
|
70
|
+
// The hub's own tag. It is exempt from the rule every other LB-* tag
|
|
71
|
+
// follows: expansion does not require it to have a definition (build/expand.ts),
|
|
72
|
+
// and the client build does not require it to have a script of its own
|
|
73
|
+
// (build/elements.ts) — it is the framework's live element, always present,
|
|
74
|
+
// registered once by the package's main entry point, not a widget an
|
|
75
|
+
// application discovers or declares.
|
|
76
|
+
export const HUB_TAG_NAME = "lb-hub";
|
|
77
|
+
// --- Request lifecycle --------------------------------------------------
|
|
78
|
+
// Stamped on the element that dispatched a request — the button, form, or
|
|
79
|
+
// widget itself — for the duration of its round trip. It carries no
|
|
80
|
+
// meaning beyond "in flight": a stylesheet dims the element, or a custom
|
|
81
|
+
// button widget watches it to disable itself, but the hub itself only sets
|
|
82
|
+
// and clears it.
|
|
83
|
+
export const ATTR_PENDING = "data-lb-pending";
|
|
84
|
+
// Stamped on the same element if that round trip failed — a non-2xx
|
|
85
|
+
// response, a thrown application error, or a timeout, all alike. Cleared at
|
|
86
|
+
// the start of that element's next request. What (if anything) an
|
|
87
|
+
// application shows for it is undecided; this is only the fact that it
|
|
88
|
+
// happened.
|
|
89
|
+
export const ATTR_ERROR = "data-lb-error";
|
|
90
|
+
// --- Wire --------------------------------------------------------------
|
|
91
|
+
export const LB_DATA_ENDPOINT = "/lb/data";
|
|
92
|
+
export const LB_REQUEST_ENDPOINT = "/lb";
|
|
93
|
+
// A client-side deadline for one round trip. fetch() has no deadline of its
|
|
94
|
+
// own; past this the request is aborted and treated as a failure.
|
|
95
|
+
export const LB_REQUEST_TIMEOUT_MS = 10_000;
|