@async/framework 0.1.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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +608 -0
- package/examples/cache/index.html +16 -0
- package/examples/cache/main.js +47 -0
- package/examples/components/index.html +11 -0
- package/examples/components/main.js +26 -0
- package/examples/counter/index.html +15 -0
- package/examples/counter/main.js +17 -0
- package/examples/partials/index.html +15 -0
- package/examples/partials/main.js +43 -0
- package/examples/product/index.html +32 -0
- package/examples/product/main.js +24 -0
- package/examples/router/index.html +21 -0
- package/examples/router/main.js +52 -0
- package/examples/server-call/index.html +21 -0
- package/examples/server-call/main.js +22 -0
- package/examples/ssr/index.html +12 -0
- package/examples/ssr/main.js +89 -0
- package/examples/streaming/index.html +16 -0
- package/examples/streaming/main.js +30 -0
- package/package.json +67 -0
- package/src/app.js +383 -0
- package/src/async-signal.js +238 -0
- package/src/cache.js +145 -0
- package/src/component.js +182 -0
- package/src/delay.js +30 -0
- package/src/handlers.js +175 -0
- package/src/html.js +65 -0
- package/src/index.js +12 -0
- package/src/loader.js +394 -0
- package/src/partials.js +96 -0
- package/src/router.js +367 -0
- package/src/server.js +369 -0
- package/src/signals.js +483 -0
package/src/loader.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { renderComponent } from "./component.js";
|
|
2
|
+
import { createHandlerRegistry } from "./handlers.js";
|
|
3
|
+
import { createSignalRegistry } from "./signals.js";
|
|
4
|
+
|
|
5
|
+
export function AsyncLoader({ root, signals, handlers, server, router, cache } = {}) {
|
|
6
|
+
const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
|
|
7
|
+
const rootNode = root ?? documentRef;
|
|
8
|
+
const signalRegistry = signals ?? createSignalRegistry();
|
|
9
|
+
const handlerRegistry = handlers ?? createHandlerRegistry();
|
|
10
|
+
const cleanups = new Set();
|
|
11
|
+
const eventBindings = new WeakMap();
|
|
12
|
+
const signalBindings = new WeakMap();
|
|
13
|
+
const mountedElements = new WeakSet();
|
|
14
|
+
const visibleElements = new WeakSet();
|
|
15
|
+
const boundaryState = new WeakMap();
|
|
16
|
+
const renderingBoundaries = new WeakSet();
|
|
17
|
+
let destroyed = false;
|
|
18
|
+
|
|
19
|
+
const api = {
|
|
20
|
+
root: rootNode,
|
|
21
|
+
signals: signalRegistry,
|
|
22
|
+
handlers: handlerRegistry,
|
|
23
|
+
server,
|
|
24
|
+
router,
|
|
25
|
+
cache,
|
|
26
|
+
|
|
27
|
+
start() {
|
|
28
|
+
assertActive();
|
|
29
|
+
api.scan(rootNode);
|
|
30
|
+
return api;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
scan(rootOrFragment = rootNode) {
|
|
34
|
+
assertActive();
|
|
35
|
+
bindSignalAttributes(rootOrFragment);
|
|
36
|
+
bindEventAttributes(rootOrFragment);
|
|
37
|
+
bindBoundaries(rootOrFragment);
|
|
38
|
+
runPseudoEvents(rootOrFragment);
|
|
39
|
+
return api;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
swap(boundaryId, fragmentOrTemplate) {
|
|
43
|
+
assertActive();
|
|
44
|
+
const boundary = findBoundary(rootNode, boundaryId);
|
|
45
|
+
if (!boundary) {
|
|
46
|
+
throw new Error(`Boundary "${boundaryId}" was not found.`);
|
|
47
|
+
}
|
|
48
|
+
boundary.replaceChildren(toFragment(fragmentOrTemplate, documentRef));
|
|
49
|
+
api.scan(boundary);
|
|
50
|
+
return boundary;
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
mount(target, Component, props = {}) {
|
|
54
|
+
assertActive();
|
|
55
|
+
const rendered = renderComponent(Component, props, {
|
|
56
|
+
signals: signalRegistry,
|
|
57
|
+
handlers: handlerRegistry,
|
|
58
|
+
loader: api,
|
|
59
|
+
server: api.server,
|
|
60
|
+
router: api.router,
|
|
61
|
+
cache: api.cache
|
|
62
|
+
});
|
|
63
|
+
target.replaceChildren(toFragment(rendered.html, target.ownerDocument));
|
|
64
|
+
api.scan(target);
|
|
65
|
+
rendered.mount(target);
|
|
66
|
+
rendered.visible(target, api._observeVisible);
|
|
67
|
+
cleanups.add(rendered.cleanup);
|
|
68
|
+
return rendered;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
destroy() {
|
|
72
|
+
if (destroyed) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
destroyed = true;
|
|
76
|
+
for (const cleanup of [...cleanups]) {
|
|
77
|
+
cleanup();
|
|
78
|
+
}
|
|
79
|
+
cleanups.clear();
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
_observeVisible(target, fn) {
|
|
83
|
+
return observeVisible(target, fn);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
signalRegistry._setContext?.({ server: api.server, router: api.router, loader: api, cache: api.cache });
|
|
88
|
+
|
|
89
|
+
function bindEventAttributes(scope) {
|
|
90
|
+
for (const element of selectAll(scope, "[data-async-container], *")) {
|
|
91
|
+
if (typeof element.getAttributeNames !== "function") {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
for (const name of element.getAttributeNames()) {
|
|
95
|
+
if (!name.startsWith("on:")) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const eventName = name.slice(3);
|
|
99
|
+
if (eventName === "mount" || eventName === "visible") {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
bindEvent(element, eventName, element.getAttribute(name));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function bindEvent(element, eventName, ref) {
|
|
108
|
+
const key = `${eventName}:${ref}`;
|
|
109
|
+
const bound = eventBindings.get(element) ?? new Set();
|
|
110
|
+
if (bound.has(key)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
bound.add(key);
|
|
114
|
+
eventBindings.set(element, bound);
|
|
115
|
+
|
|
116
|
+
const listener = async (event) => {
|
|
117
|
+
try {
|
|
118
|
+
await handlerRegistry.run(ref, {
|
|
119
|
+
signals: signalRegistry,
|
|
120
|
+
handlers: handlerRegistry,
|
|
121
|
+
loader: api,
|
|
122
|
+
server: api.server,
|
|
123
|
+
router: api.router,
|
|
124
|
+
cache: api.cache,
|
|
125
|
+
event,
|
|
126
|
+
element,
|
|
127
|
+
el: element,
|
|
128
|
+
root: rootNode
|
|
129
|
+
});
|
|
130
|
+
} catch (error) {
|
|
131
|
+
dispatchAsyncError(element, error);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
element.addEventListener(eventName, listener);
|
|
136
|
+
cleanups.add(() => element.removeEventListener(eventName, listener));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function bindSignalAttributes(scope) {
|
|
140
|
+
for (const element of selectAll(scope, "[data-async-text]")) {
|
|
141
|
+
bindSignal(element, `text:${element.getAttribute("data-async-text")}`, element.getAttribute("data-async-text"), (value) => {
|
|
142
|
+
element.textContent = value ?? "";
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const element of selectAll(scope, "[data-async-value]")) {
|
|
147
|
+
const path = element.getAttribute("data-async-value");
|
|
148
|
+
bindSignal(element, `value:${path}`, path, (value) => {
|
|
149
|
+
if ("value" in element && element.value !== String(value ?? "")) {
|
|
150
|
+
element.value = value ?? "";
|
|
151
|
+
} else if (!("value" in element)) {
|
|
152
|
+
element.setAttribute("value", value ?? "");
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
bindValueWriter(element, path);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const element of selectAll(scope, "*")) {
|
|
159
|
+
for (const name of element.getAttributeNames?.() ?? []) {
|
|
160
|
+
if (name.startsWith("data-async-attr:")) {
|
|
161
|
+
const attr = name.slice("data-async-attr:".length);
|
|
162
|
+
const path = element.getAttribute(name);
|
|
163
|
+
bindSignal(element, `attr:${attr}:${path}`, path, (value) => updateAttribute(element, attr, value));
|
|
164
|
+
}
|
|
165
|
+
if (name.startsWith("data-async-class:")) {
|
|
166
|
+
const className = name.slice("data-async-class:".length);
|
|
167
|
+
const path = element.getAttribute(name);
|
|
168
|
+
bindSignal(element, `class:${className}:${path}`, path, (value) => {
|
|
169
|
+
element.classList.toggle(className, Boolean(value));
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function bindSignal(element, key, path, apply) {
|
|
177
|
+
const bound = signalBindings.get(element) ?? new Set();
|
|
178
|
+
if (bound.has(key)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
bound.add(key);
|
|
182
|
+
signalBindings.set(element, bound);
|
|
183
|
+
|
|
184
|
+
apply(signalRegistry.get(path));
|
|
185
|
+
cleanups.add(signalRegistry.subscribe(path, apply));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function bindValueWriter(element, path) {
|
|
189
|
+
bindEvent(element, "input", `__async:set:${path}`);
|
|
190
|
+
bindEvent(element, "change", `__async:set:${path}`);
|
|
191
|
+
if (!handlerRegistry.resolve(`__async:set:${path}`)) {
|
|
192
|
+
handlerRegistry.register(`__async:set:${path}`, function writeValue({ element }) {
|
|
193
|
+
signalRegistry.set(path, element.value);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function bindBoundaries(scope) {
|
|
199
|
+
for (const boundary of selectAll(scope, "[data-async-boundary]")) {
|
|
200
|
+
if (renderingBoundaries.has(boundary)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const id = boundary.getAttribute("data-async-boundary");
|
|
204
|
+
if (!boundaryState.has(boundary)) {
|
|
205
|
+
const templates = collectBoundaryTemplates(boundary, id);
|
|
206
|
+
if (Object.keys(templates).length === 0 || !signalRegistry.has(id)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const state = {
|
|
210
|
+
id,
|
|
211
|
+
templates,
|
|
212
|
+
cleanup: signalRegistry.subscribe(`${id}.$status`, () => renderBoundary(boundary))
|
|
213
|
+
};
|
|
214
|
+
boundaryState.set(boundary, state);
|
|
215
|
+
cleanups.add(state.cleanup);
|
|
216
|
+
}
|
|
217
|
+
renderBoundary(boundary);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function renderBoundary(boundary) {
|
|
222
|
+
const state = boundaryState.get(boundary);
|
|
223
|
+
if (!state) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const status = signalRegistry.get(`${state.id}.$status`);
|
|
227
|
+
const template = chooseBoundaryTemplate(state.templates, status);
|
|
228
|
+
if (!template) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
boundary.replaceChildren(template.content.cloneNode(true));
|
|
232
|
+
renderingBoundaries.add(boundary);
|
|
233
|
+
try {
|
|
234
|
+
api.scan(boundary);
|
|
235
|
+
} finally {
|
|
236
|
+
renderingBoundaries.delete(boundary);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function runPseudoEvents(scope) {
|
|
241
|
+
for (const element of selectAll(scope, "[on\\:mount]")) {
|
|
242
|
+
if (mountedElements.has(element)) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
mountedElements.add(element);
|
|
246
|
+
runPseudo(element, element.getAttribute("on:mount"));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const element of selectAll(scope, "[on\\:visible]")) {
|
|
250
|
+
if (visibleElements.has(element)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
visibleElements.add(element);
|
|
254
|
+
cleanups.add(observeVisible(element, () => runPseudo(element, element.getAttribute("on:visible"))));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function runPseudo(element, ref) {
|
|
259
|
+
try {
|
|
260
|
+
const results = await handlerRegistry.run(ref, {
|
|
261
|
+
signals: signalRegistry,
|
|
262
|
+
handlers: handlerRegistry,
|
|
263
|
+
loader: api,
|
|
264
|
+
server: api.server,
|
|
265
|
+
router: api.router,
|
|
266
|
+
cache: api.cache,
|
|
267
|
+
element,
|
|
268
|
+
el: element,
|
|
269
|
+
root: rootNode
|
|
270
|
+
});
|
|
271
|
+
for (const result of results) {
|
|
272
|
+
if (typeof result === "function") {
|
|
273
|
+
cleanups.add(result);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
dispatchAsyncError(element, error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function observeVisible(target, fn) {
|
|
282
|
+
const ownerWindow = target.ownerDocument?.defaultView ?? globalThis;
|
|
283
|
+
const Observer = ownerWindow.IntersectionObserver ?? globalThis.IntersectionObserver;
|
|
284
|
+
if (!Observer) {
|
|
285
|
+
queueMicrotask(() => {
|
|
286
|
+
if (!destroyed) {
|
|
287
|
+
fn(target);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
return () => {};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const observer = new Observer((entries) => {
|
|
294
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
295
|
+
observer.disconnect();
|
|
296
|
+
fn(target);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
observer.observe(target);
|
|
300
|
+
return () => observer.disconnect();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function assertActive() {
|
|
304
|
+
if (destroyed) {
|
|
305
|
+
throw new Error("AsyncLoader has been destroyed.");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return api;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function collectBoundaryTemplates(boundary, id) {
|
|
313
|
+
const templates = {};
|
|
314
|
+
for (const template of [...boundary.children].filter((child) => child.tagName === "TEMPLATE")) {
|
|
315
|
+
if (template.getAttribute("data-async-loading") === id) {
|
|
316
|
+
templates.loading = template;
|
|
317
|
+
}
|
|
318
|
+
if (template.getAttribute("data-async-ready") === id) {
|
|
319
|
+
templates.ready = template;
|
|
320
|
+
}
|
|
321
|
+
if (template.getAttribute("data-async-error") === id) {
|
|
322
|
+
templates.error = template;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return templates;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function chooseBoundaryTemplate(templates, status) {
|
|
329
|
+
if (status === "ready") {
|
|
330
|
+
return templates.ready ?? templates.loading ?? templates.error;
|
|
331
|
+
}
|
|
332
|
+
if (status === "error") {
|
|
333
|
+
return templates.error ?? templates.ready ?? templates.loading;
|
|
334
|
+
}
|
|
335
|
+
return templates.loading ?? templates.ready ?? templates.error;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function updateAttribute(element, attr, value) {
|
|
339
|
+
if (value === false || value == null) {
|
|
340
|
+
element.removeAttribute(attr);
|
|
341
|
+
if (attr in element) {
|
|
342
|
+
element[attr] = false;
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
element.setAttribute(attr, value === true ? "" : String(value));
|
|
347
|
+
if (attr in element) {
|
|
348
|
+
element[attr] = value;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function selectAll(scope, selector) {
|
|
353
|
+
const elements = [];
|
|
354
|
+
if (scope?.nodeType === 1 && scope.matches?.(selector)) {
|
|
355
|
+
elements.push(scope);
|
|
356
|
+
}
|
|
357
|
+
elements.push(...(scope?.querySelectorAll?.(selector) ?? []));
|
|
358
|
+
return elements;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function findBoundary(root, boundaryId) {
|
|
362
|
+
const selector = `[data-async-boundary="${String(boundaryId).replaceAll('"', '\\"')}"]`;
|
|
363
|
+
if (root?.nodeType === 1 && root.matches?.(selector)) {
|
|
364
|
+
return root;
|
|
365
|
+
}
|
|
366
|
+
return root?.querySelector?.(selector);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function toFragment(value, documentRef) {
|
|
370
|
+
if (value?.nodeType === 11) {
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
if (value?.tagName === "TEMPLATE") {
|
|
374
|
+
return value.content.cloneNode(true);
|
|
375
|
+
}
|
|
376
|
+
if (value?.nodeType) {
|
|
377
|
+
const fragment = documentRef.createDocumentFragment();
|
|
378
|
+
fragment.append(value);
|
|
379
|
+
return fragment;
|
|
380
|
+
}
|
|
381
|
+
const template = documentRef.createElement("template");
|
|
382
|
+
template.innerHTML = String(value ?? "");
|
|
383
|
+
return template.content.cloneNode(true);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function dispatchAsyncError(element, error) {
|
|
387
|
+
const EventCtor = element.ownerDocument?.defaultView?.CustomEvent ?? globalThis.CustomEvent;
|
|
388
|
+
element.dispatchEvent(
|
|
389
|
+
new EventCtor("async:error", {
|
|
390
|
+
bubbles: true,
|
|
391
|
+
detail: { error }
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
}
|
package/src/partials.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isTemplateResult, renderTemplate } from "./html.js";
|
|
2
|
+
|
|
3
|
+
export function createPartialRegistry(initialMap = {}) {
|
|
4
|
+
const entries = new Map();
|
|
5
|
+
|
|
6
|
+
const registry = {
|
|
7
|
+
register(id, fn) {
|
|
8
|
+
assertId(id);
|
|
9
|
+
if (typeof fn !== "function") {
|
|
10
|
+
throw new TypeError(`Partial "${id}" must be a function.`);
|
|
11
|
+
}
|
|
12
|
+
if (entries.has(id)) {
|
|
13
|
+
throw new Error(`Partial "${id}" is already registered.`);
|
|
14
|
+
}
|
|
15
|
+
entries.set(id, fn);
|
|
16
|
+
return id;
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
registerMany(map) {
|
|
20
|
+
for (const [id, fn] of Object.entries(map ?? {})) {
|
|
21
|
+
registry.register(id, fn);
|
|
22
|
+
}
|
|
23
|
+
return registry;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
resolve(id) {
|
|
27
|
+
assertId(id);
|
|
28
|
+
return entries.get(id);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async render(id, props = {}, context = {}) {
|
|
32
|
+
assertId(id);
|
|
33
|
+
const fn = registry.resolve(id);
|
|
34
|
+
if (!fn) {
|
|
35
|
+
throw new Error(`Partial "${id}" is not registered.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const partialContext = {
|
|
39
|
+
...context,
|
|
40
|
+
id,
|
|
41
|
+
props,
|
|
42
|
+
cache: context.cache,
|
|
43
|
+
partials: registry
|
|
44
|
+
};
|
|
45
|
+
const result = await fn.call(partialContext, props);
|
|
46
|
+
return normalizePartialResult(result);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
registry.registerMany(initialMap);
|
|
51
|
+
return registry;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizePartialResult(result) {
|
|
55
|
+
if (isPartialEnvelope(result)) {
|
|
56
|
+
return {
|
|
57
|
+
...result,
|
|
58
|
+
html: Object.hasOwn(result, "html") ? renderPartialValue(result.html) : result.html
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { html: renderPartialValue(result) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderPartialValue(value) {
|
|
66
|
+
if (value?.nodeType) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
if (isTemplateResult(value)) {
|
|
73
|
+
return renderTemplate(value);
|
|
74
|
+
}
|
|
75
|
+
return renderTemplate(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isPartialEnvelope(value) {
|
|
79
|
+
return Boolean(
|
|
80
|
+
value &&
|
|
81
|
+
typeof value === "object" &&
|
|
82
|
+
!Array.isArray(value) &&
|
|
83
|
+
(Object.hasOwn(value, "html") ||
|
|
84
|
+
Object.hasOwn(value, "signals") ||
|
|
85
|
+
Object.hasOwn(value, "boundary") ||
|
|
86
|
+
Object.hasOwn(value, "redirect") ||
|
|
87
|
+
Object.hasOwn(value, "status") ||
|
|
88
|
+
Object.hasOwn(value, "cache"))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertId(id) {
|
|
93
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
94
|
+
throw new TypeError("Partial id must be a non-empty string.");
|
|
95
|
+
}
|
|
96
|
+
}
|