@async/framework 0.9.0 → 0.10.1
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 +20 -1
- package/README.md +115 -0
- package/browser.d.ts +69 -18
- package/browser.js +733 -71
- package/browser.min.js +1 -1
- package/browser.ts +733 -71
- package/browser.umd.js +733 -71
- package/browser.umd.min.js +1 -1
- package/package.json +11 -4
- package/server.d.ts +69 -18
- package/src/app.js +314 -46
- package/src/browser.js +2 -0
- package/src/component.js +19 -2
- package/src/elements.js +63 -0
- package/src/handlers.js +19 -2
- package/src/index.js +2 -0
- package/src/lazy-registry.js +204 -0
- package/src/loader.js +23 -5
- package/src/partials.js +19 -2
- package/src/registry-store.js +15 -9
- package/src/signals.js +46 -4
package/src/component.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { attributeName } from "./attributes.js";
|
|
2
2
|
import { escapeHtml, rawHtml, renderTemplate } from "./html.js";
|
|
3
3
|
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
4
|
+
import { createLazyRegistry, isLazyDescriptor } from "./lazy-registry.js";
|
|
4
5
|
|
|
5
6
|
const componentKind = Symbol.for("@async/framework.component");
|
|
6
7
|
let componentCounter = 0;
|
|
@@ -22,13 +23,15 @@ export function createComponentRegistry(initialMap = {}, options = {}) {
|
|
|
22
23
|
const registryStore = options.registry ?? createRegistryStore();
|
|
23
24
|
const type = options.type ?? "component";
|
|
24
25
|
const entries = registryStore._map(type);
|
|
26
|
+
const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
|
|
27
|
+
const lazyComponents = new Map();
|
|
25
28
|
|
|
26
29
|
const registry = attachRegistryInspection({
|
|
27
30
|
register(id, Component) {
|
|
28
31
|
if (typeof id !== "string" || id.length === 0) {
|
|
29
32
|
throw new TypeError("Component id must be a non-empty string.");
|
|
30
33
|
}
|
|
31
|
-
if (!isComponent(Component) && typeof Component !== "function") {
|
|
34
|
+
if (!isComponent(Component) && typeof Component !== "function" && !isLazyDescriptor(Component)) {
|
|
32
35
|
throw new TypeError(`Component "${id}" must be a component function.`);
|
|
33
36
|
}
|
|
34
37
|
if (entries.has(id)) {
|
|
@@ -49,6 +52,7 @@ export function createComponentRegistry(initialMap = {}, options = {}) {
|
|
|
49
52
|
if (typeof id !== "string" || id.length === 0) {
|
|
50
53
|
throw new TypeError("Component id must be a non-empty string.");
|
|
51
54
|
}
|
|
55
|
+
lazyComponents.delete(id);
|
|
52
56
|
return entries.delete(id);
|
|
53
57
|
},
|
|
54
58
|
|
|
@@ -56,7 +60,20 @@ export function createComponentRegistry(initialMap = {}, options = {}) {
|
|
|
56
60
|
if (typeof id !== "string" || id.length === 0) {
|
|
57
61
|
throw new TypeError("Component id must be a non-empty string.");
|
|
58
62
|
}
|
|
59
|
-
|
|
63
|
+
const Component = entries.get(id);
|
|
64
|
+
if (!isLazyDescriptor(Component)) {
|
|
65
|
+
return Component;
|
|
66
|
+
}
|
|
67
|
+
if (!lazyComponents.has(id)) {
|
|
68
|
+
lazyComponents.set(id, async function LazyComponent(...args) {
|
|
69
|
+
const resolved = await lazyRegistry.resolve(type, id, Component);
|
|
70
|
+
if (typeof resolved !== "function") {
|
|
71
|
+
throw new TypeError(`Component "${id}" did not resolve to a function.`);
|
|
72
|
+
}
|
|
73
|
+
return resolved.apply(this, args);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return lazyComponents.get(id);
|
|
60
77
|
},
|
|
61
78
|
|
|
62
79
|
_adoptMany() {
|
package/src/elements.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Async } from "./app.js";
|
|
2
|
+
|
|
3
|
+
export function defineAsyncContainerElement(options = {}) {
|
|
4
|
+
const tagName = options.tagName ?? "async-container";
|
|
5
|
+
const registry = options.customElements ?? globalThis.customElements;
|
|
6
|
+
if (!registry) {
|
|
7
|
+
throw new Error("defineAsyncContainerElement(...) requires customElements.");
|
|
8
|
+
}
|
|
9
|
+
const existing = registry.get(tagName);
|
|
10
|
+
if (existing) {
|
|
11
|
+
return existing;
|
|
12
|
+
}
|
|
13
|
+
const app = options.app ?? options.Async ?? Async;
|
|
14
|
+
const HTMLElementBase = options.HTMLElement ?? options.window?.HTMLElement ?? globalThis.HTMLElement;
|
|
15
|
+
if (!HTMLElementBase) {
|
|
16
|
+
throw new Error("defineAsyncContainerElement(...) requires HTMLElement.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class AsyncContainerElement extends HTMLElementBase {
|
|
20
|
+
connectedCallback() {
|
|
21
|
+
if (this.__asyncAttached) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const runtime = app.runtime ?? app.start?.();
|
|
25
|
+
runtime?.attachRoot?.(this);
|
|
26
|
+
this.__asyncRuntime = runtime;
|
|
27
|
+
this.__asyncAttached = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
disconnectedCallback() {
|
|
31
|
+
if (!this.__asyncAttached) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.__asyncRuntime?.detachRoot?.(this);
|
|
35
|
+
this.__asyncRuntime = undefined;
|
|
36
|
+
this.__asyncAttached = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
registry.define(tagName, AsyncContainerElement);
|
|
41
|
+
return AsyncContainerElement;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function defineAsyncSuspenseElement(options = {}) {
|
|
45
|
+
const tagName = options.tagName ?? "async-suspense";
|
|
46
|
+
const registry = options.customElements ?? globalThis.customElements;
|
|
47
|
+
if (!registry) {
|
|
48
|
+
throw new Error("defineAsyncSuspenseElement(...) requires customElements.");
|
|
49
|
+
}
|
|
50
|
+
const existing = registry.get(tagName);
|
|
51
|
+
if (existing) {
|
|
52
|
+
return existing;
|
|
53
|
+
}
|
|
54
|
+
const HTMLElementBase = options.HTMLElement ?? options.window?.HTMLElement ?? globalThis.HTMLElement;
|
|
55
|
+
if (!HTMLElementBase) {
|
|
56
|
+
throw new Error("defineAsyncSuspenseElement(...) requires HTMLElement.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class AsyncSuspenseElement extends HTMLElementBase {}
|
|
60
|
+
|
|
61
|
+
registry.define(tagName, AsyncSuspenseElement);
|
|
62
|
+
return AsyncSuspenseElement;
|
|
63
|
+
}
|
package/src/handlers.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
unwrapServerResult
|
|
6
6
|
} from "./server.js";
|
|
7
7
|
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
8
|
+
import { createLazyRegistry, isLazyDescriptor } from "./lazy-registry.js";
|
|
8
9
|
|
|
9
10
|
const builtInTokens = new Set(["prevent", "preventDefault", "stopPropagation", "stopImmediatePropagation"]);
|
|
10
11
|
const builtInHandlers = {
|
|
@@ -26,11 +27,13 @@ export function createHandlerRegistry(initialMap = {}, options = {}) {
|
|
|
26
27
|
const registryStore = options.registry ?? createRegistryStore();
|
|
27
28
|
const type = options.type ?? "handler";
|
|
28
29
|
const handlers = registryStore._map(type);
|
|
30
|
+
const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
|
|
31
|
+
const lazyHandlers = new Map();
|
|
29
32
|
|
|
30
33
|
const registry = attachRegistryInspection({
|
|
31
34
|
register(id, fn) {
|
|
32
35
|
assertId(id);
|
|
33
|
-
if (typeof fn !== "function") {
|
|
36
|
+
if (typeof fn !== "function" && !isLazyDescriptor(fn)) {
|
|
34
37
|
throw new TypeError(`Handler "${id}" must be a function.`);
|
|
35
38
|
}
|
|
36
39
|
if (handlers.has(id)) {
|
|
@@ -49,12 +52,26 @@ export function createHandlerRegistry(initialMap = {}, options = {}) {
|
|
|
49
52
|
|
|
50
53
|
unregister(id) {
|
|
51
54
|
assertId(id);
|
|
55
|
+
lazyHandlers.delete(id);
|
|
52
56
|
return handlers.delete(id);
|
|
53
57
|
},
|
|
54
58
|
|
|
55
59
|
resolve(id) {
|
|
56
60
|
assertId(id);
|
|
57
|
-
|
|
61
|
+
const handler = handlers.get(id);
|
|
62
|
+
if (!isLazyDescriptor(handler)) {
|
|
63
|
+
return handler;
|
|
64
|
+
}
|
|
65
|
+
if (!lazyHandlers.has(id)) {
|
|
66
|
+
lazyHandlers.set(id, async function runLazyHandler(...args) {
|
|
67
|
+
const resolved = await lazyRegistry.resolve(type, id, handler);
|
|
68
|
+
if (typeof resolved !== "function") {
|
|
69
|
+
throw new TypeError(`Handler "${id}" did not resolve to a function.`);
|
|
70
|
+
}
|
|
71
|
+
return resolved.apply(this, args);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return lazyHandlers.get(id);
|
|
58
75
|
},
|
|
59
76
|
|
|
60
77
|
async run(ref, context = {}) {
|
package/src/index.js
CHANGED
|
@@ -5,8 +5,10 @@ export { createBoundaryReceiver } from "./boundary-receiver.js";
|
|
|
5
5
|
export { createCacheRegistry, defineCache } from "./cache.js";
|
|
6
6
|
export { component, createComponentRegistry, defineComponent } from "./component.js";
|
|
7
7
|
export { delay } from "./delay.js";
|
|
8
|
+
export { defineAsyncContainerElement, defineAsyncSuspenseElement } from "./elements.js";
|
|
8
9
|
export { createHandlerRegistry } from "./handlers.js";
|
|
9
10
|
export { html } from "./html.js";
|
|
11
|
+
export { createLazyRegistry, defineRegistrySnapshot } from "./lazy-registry.js";
|
|
10
12
|
export { Loader, AsyncLoader } from "./loader.js";
|
|
11
13
|
export { createPartialRegistry } from "./partials.js";
|
|
12
14
|
export { createRegistryStore } from "./registry-store.js";
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const descriptorTypes = new Set(["handler", "component", "asyncSignal", "partial", "route"]);
|
|
2
|
+
const defaultBaseUrl = "_async";
|
|
3
|
+
|
|
4
|
+
export function defineRegistrySnapshot(snapshot = {}) {
|
|
5
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
|
6
|
+
throw new TypeError("defineRegistrySnapshot(snapshot) requires an object.");
|
|
7
|
+
}
|
|
8
|
+
return snapshot;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createLazyRegistry(options = {}) {
|
|
12
|
+
const registryAssets = normalizeRegistryAssets(options.registryAssets ?? options.assets);
|
|
13
|
+
const importModule = options.importModule ?? ((url) => import(url));
|
|
14
|
+
const moduleCache = new Map();
|
|
15
|
+
const exportCache = new Map();
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
registryAssets,
|
|
19
|
+
|
|
20
|
+
resolveUrl(type, id, descriptor) {
|
|
21
|
+
return resolveDescriptorUrl(type, id, descriptor, registryAssets);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async resolve(type, id, descriptor) {
|
|
25
|
+
if (!isLazyDescriptor(descriptor)) {
|
|
26
|
+
return descriptor;
|
|
27
|
+
}
|
|
28
|
+
const cacheKey = `${type}:${id}`;
|
|
29
|
+
if (exportCache.has(cacheKey)) {
|
|
30
|
+
return exportCache.get(cacheKey);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const resolved = resolveDescriptorUrl(type, id, descriptor, registryAssets);
|
|
34
|
+
let modulePromise = moduleCache.get(resolved.moduleUrl);
|
|
35
|
+
if (!modulePromise) {
|
|
36
|
+
modulePromise = Promise.resolve(importModule(resolved.moduleUrl));
|
|
37
|
+
moduleCache.set(resolved.moduleUrl, modulePromise);
|
|
38
|
+
}
|
|
39
|
+
const module = await modulePromise;
|
|
40
|
+
const value = resolveExport(module, resolved.exportNames, type, id);
|
|
41
|
+
exportCache.set(cacheKey, value);
|
|
42
|
+
return value;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
inspect() {
|
|
46
|
+
return {
|
|
47
|
+
registryAssets,
|
|
48
|
+
modules: [...moduleCache.keys()],
|
|
49
|
+
exports: [...exportCache.keys()]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeRegistryAssets(options = {}) {
|
|
56
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? defaultBaseUrl);
|
|
57
|
+
const paths = {
|
|
58
|
+
component: "component",
|
|
59
|
+
handler: "handler",
|
|
60
|
+
asyncSignal: "asyncSignal",
|
|
61
|
+
partial: "partial",
|
|
62
|
+
route: "route",
|
|
63
|
+
...(options.paths ?? {})
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
for (const [type, value] of Object.entries(paths)) {
|
|
67
|
+
if (!descriptorTypes.has(type)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
71
|
+
throw new TypeError(`Registry asset path for "${type}" must be a non-empty string.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
baseUrl,
|
|
77
|
+
paths
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isLazyDescriptor(value) {
|
|
82
|
+
return Boolean(
|
|
83
|
+
value &&
|
|
84
|
+
typeof value === "object" &&
|
|
85
|
+
!Array.isArray(value) &&
|
|
86
|
+
typeof value.url === "string"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function sameRegistryValue(left, right) {
|
|
91
|
+
if (left === right) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (isLazyDescriptor(left) && isLazyDescriptor(right)) {
|
|
95
|
+
return stableStringify(left) === stableStringify(right);
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function publicRegistryValue(value, id) {
|
|
101
|
+
if (isLazyDescriptor(value)) {
|
|
102
|
+
return { ...value };
|
|
103
|
+
}
|
|
104
|
+
return { id };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveDescriptorUrl(type, id, descriptor, registryAssets) {
|
|
108
|
+
if (!descriptorTypes.has(type)) {
|
|
109
|
+
throw new Error(`Registry type "${type}" does not support lazy descriptors.`);
|
|
110
|
+
}
|
|
111
|
+
if (!isLazyDescriptor(descriptor)) {
|
|
112
|
+
throw new TypeError(`Registry descriptor for "${type}:${id}" requires a url.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { path, hash } = splitHash(descriptor.url);
|
|
116
|
+
const moduleUrl = resolveModuleUrl(type, path, registryAssets);
|
|
117
|
+
const exportNames = hash
|
|
118
|
+
? [hash]
|
|
119
|
+
: inferredExportNames(id, path);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
moduleUrl,
|
|
123
|
+
exportNames,
|
|
124
|
+
url: hash ? `${moduleUrl}#${hash}` : moduleUrl
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveModuleUrl(type, path, registryAssets) {
|
|
129
|
+
if (isAbsoluteUrl(path) || path.startsWith("/") || path.startsWith("./") || path.startsWith("../")) {
|
|
130
|
+
return path;
|
|
131
|
+
}
|
|
132
|
+
const typePath = registryAssets.paths[type] ?? type;
|
|
133
|
+
return joinUrl(registryAssets.baseUrl, typePath, path);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveExport(module, exportNames, type, id) {
|
|
137
|
+
for (const name of exportNames) {
|
|
138
|
+
if (name in module) {
|
|
139
|
+
return module[name];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Lazy ${type} "${id}" did not export ${exportNames.map((name) => `"${name}"`).join(", ")}.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function inferredExportNames(id, path) {
|
|
146
|
+
const names = [];
|
|
147
|
+
const leaf = id.split(".").filter(Boolean).at(-1);
|
|
148
|
+
const basename = path
|
|
149
|
+
.split("/")
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.at(-1)
|
|
152
|
+
?.replace(/\.[^.]+$/, "");
|
|
153
|
+
for (const name of [leaf, basename, "default"]) {
|
|
154
|
+
if (name && !names.includes(name)) {
|
|
155
|
+
names.push(name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return names;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function splitHash(url) {
|
|
162
|
+
const index = url.indexOf("#");
|
|
163
|
+
if (index === -1) {
|
|
164
|
+
return { path: url, hash: "" };
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
path: url.slice(0, index),
|
|
168
|
+
hash: url.slice(index + 1)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeBaseUrl(baseUrl) {
|
|
173
|
+
if (typeof baseUrl !== "string" || baseUrl.length === 0) {
|
|
174
|
+
throw new TypeError("registryAssets.baseUrl must be a non-empty string.");
|
|
175
|
+
}
|
|
176
|
+
if (isAbsoluteUrl(baseUrl) || baseUrl.startsWith("/") || baseUrl.startsWith("./") || baseUrl.startsWith("../")) {
|
|
177
|
+
return stripTrailingSlash(baseUrl);
|
|
178
|
+
}
|
|
179
|
+
return `/${stripSlashes(baseUrl)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function joinUrl(...parts) {
|
|
183
|
+
const [first, ...rest] = parts;
|
|
184
|
+
return [stripTrailingSlash(first), ...rest.map(stripSlashes)].filter(Boolean).join("/");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function stripSlashes(value) {
|
|
188
|
+
return String(value).replace(/^\/+|\/+$/g, "");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function stripTrailingSlash(value) {
|
|
192
|
+
return String(value).replace(/\/+$/g, "");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isAbsoluteUrl(value) {
|
|
196
|
+
return /^[A-Za-z][A-Za-z\d+.-]*:/.test(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function stableStringify(value) {
|
|
200
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
201
|
+
return JSON.stringify(value);
|
|
202
|
+
}
|
|
203
|
+
return JSON.stringify(Object.keys(value).sort().map((key) => [key, value[key]]));
|
|
204
|
+
}
|
package/src/loader.js
CHANGED
|
@@ -335,7 +335,7 @@ export function Loader({ root, signals, handlers, server, router, cache, attribu
|
|
|
335
335
|
if (renderingBoundaries.has(boundary)) {
|
|
336
336
|
continue;
|
|
337
337
|
}
|
|
338
|
-
const id =
|
|
338
|
+
const id = boundaryIdFor(boundary, attributeConfig);
|
|
339
339
|
if (id == null) {
|
|
340
340
|
continue;
|
|
341
341
|
}
|
|
@@ -652,19 +652,26 @@ function writeClassTokens(element, tokens) {
|
|
|
652
652
|
function collectBoundaryTemplates(boundary, id, attributeConfig) {
|
|
653
653
|
const templates = {};
|
|
654
654
|
for (const template of [...boundary.children].filter((child) => child.tagName === "TEMPLATE")) {
|
|
655
|
-
if (
|
|
655
|
+
if (templateMatchesState(template, "loading", id, boundary, attributeConfig)) {
|
|
656
656
|
templates.loading = template;
|
|
657
657
|
}
|
|
658
|
-
if (
|
|
658
|
+
if (templateMatchesState(template, "ready", id, boundary, attributeConfig)) {
|
|
659
659
|
templates.ready = template;
|
|
660
660
|
}
|
|
661
|
-
if (
|
|
661
|
+
if (templateMatchesState(template, "error", id, boundary, attributeConfig)) {
|
|
662
662
|
templates.error = template;
|
|
663
663
|
}
|
|
664
664
|
}
|
|
665
665
|
return templates;
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
function templateMatchesState(template, state, id, boundary, attributeConfig) {
|
|
669
|
+
if (readAttribute(template, attributeConfig, "async", state) === id) {
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
return isAsyncSuspense(boundary) && template.hasAttribute?.(state);
|
|
673
|
+
}
|
|
674
|
+
|
|
668
675
|
function chooseBoundaryTemplate(templates, status) {
|
|
669
676
|
if (status === "ready") {
|
|
670
677
|
return templates.ready ?? templates.loading ?? templates.error;
|
|
@@ -712,13 +719,24 @@ function elementsIn(scope) {
|
|
|
712
719
|
|
|
713
720
|
function findBoundary(root, boundaryId, attributeConfig) {
|
|
714
721
|
for (const element of elementsIn(root)) {
|
|
715
|
-
if (
|
|
722
|
+
if (boundaryIdFor(element, attributeConfig) === String(boundaryId)) {
|
|
716
723
|
return element;
|
|
717
724
|
}
|
|
718
725
|
}
|
|
719
726
|
return null;
|
|
720
727
|
}
|
|
721
728
|
|
|
729
|
+
function boundaryIdFor(element, attributeConfig) {
|
|
730
|
+
if (isAsyncSuspense(element) && element.hasAttribute?.("for")) {
|
|
731
|
+
return element.getAttribute("for");
|
|
732
|
+
}
|
|
733
|
+
return readAttribute(element, attributeConfig, "async", "boundary");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function isAsyncSuspense(element) {
|
|
737
|
+
return element?.tagName === "ASYNC-SUSPENSE";
|
|
738
|
+
}
|
|
739
|
+
|
|
722
740
|
function toFragment(value, documentRef) {
|
|
723
741
|
if (value?.nodeType === 11) {
|
|
724
742
|
return value;
|
package/src/partials.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { isTemplateResult, renderTemplate } from "./html.js";
|
|
2
2
|
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
3
|
+
import { createLazyRegistry, isLazyDescriptor } from "./lazy-registry.js";
|
|
3
4
|
|
|
4
5
|
export function createPartialRegistry(initialMap = {}, options = {}) {
|
|
5
6
|
const registryStore = options.registry ?? createRegistryStore();
|
|
6
7
|
const type = options.type ?? "partial";
|
|
7
8
|
const entries = registryStore._map(type);
|
|
9
|
+
const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
|
|
10
|
+
const lazyPartials = new Map();
|
|
8
11
|
|
|
9
12
|
const registry = attachRegistryInspection({
|
|
10
13
|
register(id, fn) {
|
|
11
14
|
assertId(id);
|
|
12
|
-
if (typeof fn !== "function") {
|
|
15
|
+
if (typeof fn !== "function" && !isLazyDescriptor(fn)) {
|
|
13
16
|
throw new TypeError(`Partial "${id}" must be a function.`);
|
|
14
17
|
}
|
|
15
18
|
if (entries.has(id)) {
|
|
@@ -28,12 +31,26 @@ export function createPartialRegistry(initialMap = {}, options = {}) {
|
|
|
28
31
|
|
|
29
32
|
unregister(id) {
|
|
30
33
|
assertId(id);
|
|
34
|
+
lazyPartials.delete(id);
|
|
31
35
|
return entries.delete(id);
|
|
32
36
|
},
|
|
33
37
|
|
|
34
38
|
resolve(id) {
|
|
35
39
|
assertId(id);
|
|
36
|
-
|
|
40
|
+
const partial = entries.get(id);
|
|
41
|
+
if (!isLazyDescriptor(partial)) {
|
|
42
|
+
return partial;
|
|
43
|
+
}
|
|
44
|
+
if (!lazyPartials.has(id)) {
|
|
45
|
+
lazyPartials.set(id, async function runLazyPartial(...args) {
|
|
46
|
+
const resolved = await lazyRegistry.resolve(type, id, partial);
|
|
47
|
+
if (typeof resolved !== "function") {
|
|
48
|
+
throw new TypeError(`Partial "${id}" did not resolve to a function.`);
|
|
49
|
+
}
|
|
50
|
+
return resolved.apply(this, args);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return lazyPartials.get(id);
|
|
37
54
|
},
|
|
38
55
|
|
|
39
56
|
async render(id, props = {}, context = {}) {
|
package/src/registry-store.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { publicRegistryValue } from "./lazy-registry.js";
|
|
2
|
+
|
|
3
|
+
const declarationTypes = new Set(["signal", "handler", "server", "partial", "route", "component", "asyncSignal"]);
|
|
2
4
|
const cacheTypes = new Set(["cache.browser", "cache.server"]);
|
|
3
5
|
const cacheEntryTypes = new Set(["cache.browser.entries", "cache.server.entries"]);
|
|
4
6
|
const allTypes = new Set([...declarationTypes, ...cacheTypes, ...cacheEntryTypes]);
|
|
@@ -85,11 +87,12 @@ export function createRegistryStore(initial = {}, options = {}) {
|
|
|
85
87
|
const snapshotTarget = snapshotOptions.target ?? target;
|
|
86
88
|
return {
|
|
87
89
|
signal: snapshotSignals(backing.signal),
|
|
88
|
-
handler: snapshotDescriptors(backing.handler
|
|
89
|
-
server: snapshotDescriptors(backing.server
|
|
90
|
-
partial: snapshotDescriptors(backing.partial
|
|
90
|
+
handler: snapshotDescriptors(backing.handler),
|
|
91
|
+
server: snapshotDescriptors(backing.server),
|
|
92
|
+
partial: snapshotDescriptors(backing.partial),
|
|
91
93
|
route: snapshotPlain(backing.route),
|
|
92
|
-
component: snapshotDescriptors(backing.component
|
|
94
|
+
component: snapshotDescriptors(backing.component),
|
|
95
|
+
asyncSignal: snapshotDescriptors(backing.asyncSignal),
|
|
93
96
|
cache: {
|
|
94
97
|
browser: snapshotPlain(backing.cache.browser),
|
|
95
98
|
server: snapshotPlain(backing.cache.server)
|
|
@@ -109,6 +112,7 @@ export function createRegistryStore(initial = {}, options = {}) {
|
|
|
109
112
|
partial: Object.fromEntries(backing.partial),
|
|
110
113
|
route: Object.fromEntries(backing.route),
|
|
111
114
|
component: Object.fromEntries(backing.component),
|
|
115
|
+
asyncSignal: Object.fromEntries(backing.asyncSignal),
|
|
112
116
|
cache: {
|
|
113
117
|
browser: Object.fromEntries(backing.cache.browser),
|
|
114
118
|
server: Object.fromEntries(backing.cache.server)
|
|
@@ -168,6 +172,7 @@ function createBacking() {
|
|
|
168
172
|
partial: new Map(),
|
|
169
173
|
route: new Map(),
|
|
170
174
|
component: new Map(),
|
|
175
|
+
asyncSignal: new Map(),
|
|
171
176
|
cache: {
|
|
172
177
|
browser: new Map(),
|
|
173
178
|
server: new Map()
|
|
@@ -186,6 +191,7 @@ function applyInitial(registry, initial = {}) {
|
|
|
186
191
|
registry.registerMany("partial", initial.partial);
|
|
187
192
|
registry.registerMany("route", initial.route);
|
|
188
193
|
registry.registerMany("component", initial.component);
|
|
194
|
+
registry.registerMany("asyncSignal", initial.asyncSignal);
|
|
189
195
|
registry.registerMany("cache.browser", initial.cache?.browser);
|
|
190
196
|
registry.registerMany("cache.server", initial.cache?.server);
|
|
191
197
|
|
|
@@ -213,7 +219,7 @@ function assertId(type, id) {
|
|
|
213
219
|
|
|
214
220
|
function publicValue(type, id, value, options) {
|
|
215
221
|
if (type === "server" && options.target === "browser") {
|
|
216
|
-
return
|
|
222
|
+
return publicRegistryValue(value, id);
|
|
217
223
|
}
|
|
218
224
|
if (cacheEntryTypes.has(type)) {
|
|
219
225
|
return value?.value;
|
|
@@ -233,10 +239,10 @@ function snapshotSignals(map) {
|
|
|
233
239
|
return snapshot;
|
|
234
240
|
}
|
|
235
241
|
|
|
236
|
-
function snapshotDescriptors(map
|
|
242
|
+
function snapshotDescriptors(map) {
|
|
237
243
|
const snapshot = {};
|
|
238
|
-
for (const id of map
|
|
239
|
-
snapshot[id] =
|
|
244
|
+
for (const [id, value] of map) {
|
|
245
|
+
snapshot[id] = publicRegistryValue(value, id);
|
|
240
246
|
}
|
|
241
247
|
return snapshot;
|
|
242
248
|
}
|
package/src/signals.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { asyncSignal as createAsyncSignal, isAsyncSignal } from "./async-signal.js";
|
|
2
2
|
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
3
|
+
import { createLazyRegistry, isLazyDescriptor } from "./lazy-registry.js";
|
|
3
4
|
|
|
4
5
|
const signalKind = Symbol.for("@async/framework.signal");
|
|
5
6
|
const computedKind = Symbol.for("@async/framework.computed");
|
|
@@ -122,6 +123,8 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
|
|
|
122
123
|
const registryStore = options.registry ?? createRegistryStore();
|
|
123
124
|
const type = options.type ?? "signal";
|
|
124
125
|
const entries = registryStore._map(type);
|
|
126
|
+
const asyncDescriptors = registryStore._map("asyncSignal");
|
|
127
|
+
const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
|
|
125
128
|
const registryCleanups = new Map();
|
|
126
129
|
const runtimeContext = {};
|
|
127
130
|
const boundEntries = new Set();
|
|
@@ -162,6 +165,7 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
|
|
|
162
165
|
|
|
163
166
|
ensure(id, initial) {
|
|
164
167
|
assertId(id);
|
|
168
|
+
materializeAsyncSignal(id);
|
|
165
169
|
if (!entries.has(id)) {
|
|
166
170
|
registry.register(id, createSignal(initial));
|
|
167
171
|
}
|
|
@@ -169,18 +173,18 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
|
|
|
169
173
|
},
|
|
170
174
|
|
|
171
175
|
has(id) {
|
|
172
|
-
return entries.has(id);
|
|
176
|
+
return entries.has(id) || asyncDescriptors.has(id);
|
|
173
177
|
},
|
|
174
178
|
|
|
175
179
|
get(path) {
|
|
176
|
-
const parsed =
|
|
180
|
+
const parsed = parseRegistryPath(path);
|
|
177
181
|
track(parsed.path);
|
|
178
182
|
const entry = requireEntry(entries, parsed.id);
|
|
179
183
|
return readEntry(entry, parsed.parts);
|
|
180
184
|
},
|
|
181
185
|
|
|
182
186
|
set(path, value) {
|
|
183
|
-
const parsed =
|
|
187
|
+
const parsed = parseRegistryPath(path);
|
|
184
188
|
const entry = requireEntry(entries, parsed.id);
|
|
185
189
|
if (parsed.parts.length === 0) {
|
|
186
190
|
return entry.set(value);
|
|
@@ -199,6 +203,7 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
|
|
|
199
203
|
|
|
200
204
|
ref(id) {
|
|
201
205
|
assertId(id);
|
|
206
|
+
materializeAsyncSignal(id);
|
|
202
207
|
return createRef(registry, id);
|
|
203
208
|
},
|
|
204
209
|
|
|
@@ -206,7 +211,7 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
|
|
|
206
211
|
if (typeof fn !== "function") {
|
|
207
212
|
throw new TypeError("subscribe(path, fn) requires a function.");
|
|
208
213
|
}
|
|
209
|
-
const parsed =
|
|
214
|
+
const parsed = parseRegistryPath(path);
|
|
210
215
|
const entry = requireEntry(entries, parsed.id);
|
|
211
216
|
const subscriptionId = ++subscriptionCounter;
|
|
212
217
|
return entry.subscribe(() => {
|
|
@@ -312,6 +317,7 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
|
|
|
312
317
|
},
|
|
313
318
|
|
|
314
319
|
_entry(id) {
|
|
320
|
+
materializeAsyncSignal(id);
|
|
315
321
|
return requireEntry(entries, id);
|
|
316
322
|
},
|
|
317
323
|
|
|
@@ -351,6 +357,42 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
|
|
|
351
357
|
}
|
|
352
358
|
}
|
|
353
359
|
|
|
360
|
+
function parseRegistryPath(path) {
|
|
361
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
362
|
+
throw new TypeError("Signal path must be a non-empty string.");
|
|
363
|
+
}
|
|
364
|
+
const segments = path.split(".");
|
|
365
|
+
for (let end = segments.length; end > 0; end -= 1) {
|
|
366
|
+
const id = segments.slice(0, end).join(".");
|
|
367
|
+
if (entries.has(id) || asyncDescriptors.has(id)) {
|
|
368
|
+
materializeAsyncSignal(id);
|
|
369
|
+
return { id, parts: segments.slice(end), path };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const [id, ...parts] = segments;
|
|
373
|
+
return { id, parts, path };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function materializeAsyncSignal(id) {
|
|
377
|
+
if (entries.has(id) || !asyncDescriptors.has(id)) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const descriptor = asyncDescriptors.get(id);
|
|
381
|
+
if (!isLazyDescriptor(descriptor) && typeof descriptor !== "function") {
|
|
382
|
+
throw new TypeError(`Async signal "${id}" must be a function or lazy descriptor.`);
|
|
383
|
+
}
|
|
384
|
+
const loader = async function runLazyAsyncSignal(...args) {
|
|
385
|
+
const resolved = await lazyRegistry.resolve("asyncSignal", id, descriptor);
|
|
386
|
+
if (typeof resolved !== "function") {
|
|
387
|
+
throw new TypeError(`Async signal "${id}" did not resolve to a function.`);
|
|
388
|
+
}
|
|
389
|
+
return resolved.apply(this, args);
|
|
390
|
+
};
|
|
391
|
+
const entry = createAsyncSignal(id, loader);
|
|
392
|
+
entries.set(id, entry);
|
|
393
|
+
bindEntry(id, entry);
|
|
394
|
+
}
|
|
395
|
+
|
|
354
396
|
function scheduleCallback(fn, options = {}) {
|
|
355
397
|
const scheduler = options.scheduler;
|
|
356
398
|
if (!scheduler || options.phase === "sync") {
|