@async/framework 0.2.2 → 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/CHANGELOG.md +26 -0
- package/README.md +324 -48
- package/examples/cache/index.html +3 -3
- package/examples/components/main.js +10 -10
- package/examples/counter/index.html +2 -2
- package/examples/partials/index.html +2 -2
- package/examples/product/index.html +9 -9
- package/examples/router/index.html +2 -2
- package/examples/router/main.js +2 -2
- package/examples/server-call/index.html +2 -2
- package/examples/ssr/index.html +1 -1
- package/examples/ssr/main.js +2 -2
- package/examples/streaming/index.html +2 -2
- package/examples/streaming/main.js +2 -2
- package/framework.js +3912 -0
- package/package.json +14 -3
- package/src/app.js +73 -53
- package/src/attributes.js +52 -0
- package/src/cache.js +31 -16
- package/src/component.js +94 -19
- package/src/handlers.js +24 -5
- package/src/html.js +99 -6
- package/src/index.js +2 -0
- package/src/loader.js +291 -54
- package/src/partials.js +26 -11
- package/src/registry-store.js +257 -0
- package/src/router.js +42 -3
- package/src/server.js +12 -4
- package/src/signals.js +32 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@async/framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "No-build AsyncLoader app runtime with signals, command events, server calls, route partials, cache split, SSR activation, and streaming boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.1.0",
|
|
@@ -28,12 +28,21 @@
|
|
|
28
28
|
"web-framework"
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
|
+
"unpkg": "./framework.js",
|
|
31
32
|
"exports": {
|
|
32
|
-
".":
|
|
33
|
+
".": {
|
|
34
|
+
"unpkg": "./framework.js",
|
|
35
|
+
"browser": "./framework.js",
|
|
36
|
+
"import": "./src/index.js",
|
|
37
|
+
"default": "./src/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./framework.js": "./framework.js",
|
|
40
|
+
"./package.json": "./package.json"
|
|
33
41
|
},
|
|
34
42
|
"files": [
|
|
35
43
|
"CHANGELOG.md",
|
|
36
44
|
"README.md",
|
|
45
|
+
"framework.js",
|
|
37
46
|
"src",
|
|
38
47
|
"examples",
|
|
39
48
|
"LICENSE",
|
|
@@ -41,10 +50,12 @@
|
|
|
41
50
|
],
|
|
42
51
|
"scripts": {
|
|
43
52
|
"async-pipeline": "async-pipeline",
|
|
53
|
+
"bundle": "node scripts/build-framework-bundle.js",
|
|
54
|
+
"bundle:check": "node scripts/build-framework-bundle.js --check",
|
|
44
55
|
"docs:build": "node scripts/build-pages.js",
|
|
45
56
|
"examples": "node --test tests/examples.test.js",
|
|
46
57
|
"examples:check": "node --test tests/examples.test.js",
|
|
47
|
-
"pack:check": "npm pack --dry-run --ignore-scripts",
|
|
58
|
+
"pack:check": "pnpm run bundle:check && npm pack --dry-run --ignore-scripts",
|
|
48
59
|
"pipeline:github:check": "async-pipeline github check",
|
|
49
60
|
"pipeline:github:generate": "async-pipeline github generate",
|
|
50
61
|
"pipeline:pages": "async-pipeline run pages",
|
package/src/app.js
CHANGED
|
@@ -6,17 +6,21 @@ import { createPartialRegistry } from "./partials.js";
|
|
|
6
6
|
import { createRouteRegistry, createRouter } from "./router.js";
|
|
7
7
|
import { createServerRegistry } from "./server.js";
|
|
8
8
|
import { createSignal, createSignalRegistry } from "./signals.js";
|
|
9
|
+
import { createRegistryStore } from "./registry-store.js";
|
|
10
|
+
import { attributeName, normalizeAttributeConfig } from "./attributes.js";
|
|
9
11
|
|
|
10
12
|
const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
|
|
11
13
|
|
|
12
14
|
export function defineApp(initial) {
|
|
13
|
-
const
|
|
15
|
+
const registry = createRegistryStore(undefined, { target: "browser" });
|
|
14
16
|
const runtimes = new Set();
|
|
15
17
|
|
|
16
18
|
const app = {
|
|
19
|
+
registry,
|
|
20
|
+
|
|
17
21
|
use(typeOrModule, entries) {
|
|
18
22
|
const normalized = normalizeUse(typeOrModule, entries);
|
|
19
|
-
appendDeclarations(
|
|
23
|
+
appendDeclarations(registry, normalized);
|
|
20
24
|
for (const runtime of runtimes) {
|
|
21
25
|
runtime._applyUse(normalized);
|
|
22
26
|
}
|
|
@@ -24,7 +28,7 @@ export function defineApp(initial) {
|
|
|
24
28
|
},
|
|
25
29
|
|
|
26
30
|
snapshot() {
|
|
27
|
-
return
|
|
31
|
+
return registry.rawSnapshot();
|
|
28
32
|
},
|
|
29
33
|
|
|
30
34
|
start(options = {}) {
|
|
@@ -53,15 +57,16 @@ export function defineApp(initial) {
|
|
|
53
57
|
export function createApp(appOrDefinition = Async, options = {}) {
|
|
54
58
|
const app = isAppHub(appOrDefinition) ? appOrDefinition : defineApp(appOrDefinition ?? {});
|
|
55
59
|
const target = options.target ?? "browser";
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
60
|
+
const attributes = normalizeAttributeConfig(options.attributes);
|
|
61
|
+
const registry = options.registry ?? app.registry.view({ target });
|
|
62
|
+
const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal" });
|
|
63
|
+
const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler" });
|
|
64
|
+
const serverCache = createCacheRegistry(undefined, { registry, type: "cache.server" });
|
|
65
|
+
const browserCache = createCacheRegistry(undefined, { registry, type: "cache.browser" });
|
|
66
|
+
const server = options.server ?? createServerRegistry(undefined, { registry, type: "server" });
|
|
67
|
+
const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial" });
|
|
68
|
+
const routes = options.routes ?? createRouteRegistry(undefined, { registry, type: "route" });
|
|
69
|
+
const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component" });
|
|
65
70
|
let loader = options.loader;
|
|
66
71
|
let router = options.router;
|
|
67
72
|
let detach = () => {};
|
|
@@ -73,6 +78,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
73
78
|
|
|
74
79
|
const runtime = {
|
|
75
80
|
app,
|
|
81
|
+
registry,
|
|
76
82
|
target,
|
|
77
83
|
signals,
|
|
78
84
|
handlers,
|
|
@@ -85,6 +91,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
85
91
|
},
|
|
86
92
|
loader,
|
|
87
93
|
router,
|
|
94
|
+
attributes,
|
|
88
95
|
|
|
89
96
|
start() {
|
|
90
97
|
assertActive();
|
|
@@ -99,7 +106,8 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
99
106
|
signals,
|
|
100
107
|
handlers,
|
|
101
108
|
server,
|
|
102
|
-
cache: browserCache
|
|
109
|
+
cache: browserCache,
|
|
110
|
+
attributes
|
|
103
111
|
});
|
|
104
112
|
runtime.loader = loader;
|
|
105
113
|
|
|
@@ -121,7 +129,8 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
121
129
|
cache: browserCache,
|
|
122
130
|
partials,
|
|
123
131
|
fetch: options.fetch,
|
|
124
|
-
routeEndpoint: options.routeEndpoint
|
|
132
|
+
routeEndpoint: options.routeEndpoint,
|
|
133
|
+
attributes
|
|
125
134
|
});
|
|
126
135
|
runtime.router = router;
|
|
127
136
|
loader.router = router;
|
|
@@ -148,7 +157,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
148
157
|
const matched = routes.match(url);
|
|
149
158
|
if (!matched) {
|
|
150
159
|
return {
|
|
151
|
-
html: renderDocument("", { status: 404, signals, browserCache, boundary: options.boundary ?? "route" }),
|
|
160
|
+
html: renderDocument("", { status: 404, signals, browserCache, boundary: options.boundary ?? "route", attributes }),
|
|
152
161
|
status: 404,
|
|
153
162
|
signals: signals.snapshot(),
|
|
154
163
|
cache: { browser: browserCache.snapshot() }
|
|
@@ -182,7 +191,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
182
191
|
|
|
183
192
|
const status = result.status ?? 200;
|
|
184
193
|
return {
|
|
185
|
-
html: renderDocument(result.html, { status, signals, browserCache, boundary: result.boundary ?? options.boundary ?? "route" }),
|
|
194
|
+
html: renderDocument(result.html, { status, signals, browserCache, boundary: result.boundary ?? options.boundary ?? "route", attributes }),
|
|
186
195
|
status,
|
|
187
196
|
signals: signals.snapshot(),
|
|
188
197
|
cache: { browser: browserCache.snapshot() }
|
|
@@ -233,16 +242,25 @@ export function createApp(appOrDefinition = Async, options = {}) {
|
|
|
233
242
|
export const Async = defineApp();
|
|
234
243
|
|
|
235
244
|
function applyUseToRuntime(runtime, normalized) {
|
|
236
|
-
runtime.signals.
|
|
237
|
-
runtime.handlers.
|
|
238
|
-
|
|
239
|
-
|
|
245
|
+
applyRegistryUse(runtime.signals, runtime.registry, normalized.signal);
|
|
246
|
+
applyRegistryUse(runtime.handlers, runtime.registry, normalized.handler);
|
|
247
|
+
applyRegistryUse(runtime.server, runtime.registry, normalized.server);
|
|
248
|
+
applyRegistryUse(runtime.partials, runtime.registry, normalized.partial);
|
|
249
|
+
applyRegistryUse(runtime.routes, runtime.registry, normalized.route);
|
|
250
|
+
applyRegistryUse(runtime.components, runtime.registry, normalized.component);
|
|
251
|
+
applyRegistryUse(runtime.browser.cache, runtime.registry, normalized.cache.browser);
|
|
252
|
+
applyRegistryUse(runtime.server.cache, runtime.registry, normalized.cache.server);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function applyRegistryUse(registry, runtimeRegistry, entries) {
|
|
256
|
+
if (!entries || Object.keys(entries).length === 0) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (registry?.registry === runtimeRegistry) {
|
|
260
|
+
registry._adoptMany?.(entries);
|
|
261
|
+
return;
|
|
240
262
|
}
|
|
241
|
-
|
|
242
|
-
runtime.routes.registerMany(normalized.route);
|
|
243
|
-
runtime.components.registerMany(normalized.component);
|
|
244
|
-
runtime.browser.cache.registerMany(normalized.cache.browser);
|
|
245
|
-
runtime.server.cache.registerMany(normalized.cache.server);
|
|
263
|
+
registry?.registerMany?.(entries);
|
|
246
264
|
}
|
|
247
265
|
|
|
248
266
|
function emptyDeclarations() {
|
|
@@ -267,7 +285,7 @@ function normalizeUse(typeOrModule, entries) {
|
|
|
267
285
|
if (!registryTypes.has(typeOrModule)) {
|
|
268
286
|
throw new Error(`Unknown Async registry type "${typeOrModule}".`);
|
|
269
287
|
}
|
|
270
|
-
normalized[typeOrModule] =
|
|
288
|
+
normalized[typeOrModule] = normalizeEntries(typeOrModule, entries);
|
|
271
289
|
return normalized;
|
|
272
290
|
}
|
|
273
291
|
|
|
@@ -284,7 +302,7 @@ function normalizeUse(typeOrModule, entries) {
|
|
|
284
302
|
if (!registryTypes.has(type)) {
|
|
285
303
|
throw new Error(`Unknown Async registry type "${type}".`);
|
|
286
304
|
}
|
|
287
|
-
normalized[type] =
|
|
305
|
+
normalized[type] = normalizeEntries(type, value);
|
|
288
306
|
}
|
|
289
307
|
|
|
290
308
|
return normalized;
|
|
@@ -292,38 +310,20 @@ function normalizeUse(typeOrModule, entries) {
|
|
|
292
310
|
|
|
293
311
|
function appendDeclarations(target, source) {
|
|
294
312
|
for (const type of registryTypes) {
|
|
295
|
-
addEntries(target
|
|
313
|
+
addEntries(target, type, source[type]);
|
|
296
314
|
}
|
|
297
|
-
addEntries(target
|
|
298
|
-
addEntries(target
|
|
315
|
+
addEntries(target, "cache.browser", source.cache.browser);
|
|
316
|
+
addEntries(target, "cache.server", source.cache.server);
|
|
299
317
|
}
|
|
300
318
|
|
|
301
|
-
function addEntries(
|
|
319
|
+
function addEntries(registry, type, source) {
|
|
302
320
|
for (const [id, value] of Object.entries(source ?? {})) {
|
|
303
|
-
|
|
304
|
-
throw new Error(`${label} "${id}" is already registered.`);
|
|
305
|
-
}
|
|
306
|
-
target[id] = value;
|
|
321
|
+
registry.register(type, id, value);
|
|
307
322
|
}
|
|
308
323
|
}
|
|
309
324
|
|
|
310
|
-
function cloneDeclarations(source) {
|
|
311
|
-
return {
|
|
312
|
-
signal: { ...source.signal },
|
|
313
|
-
handler: { ...source.handler },
|
|
314
|
-
server: { ...source.server },
|
|
315
|
-
partial: { ...source.partial },
|
|
316
|
-
route: { ...source.route },
|
|
317
|
-
component: { ...source.component },
|
|
318
|
-
cache: {
|
|
319
|
-
browser: { ...source.cache.browser },
|
|
320
|
-
server: { ...source.cache.server }
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
325
|
function isAppHub(value) {
|
|
326
|
-
return Boolean(value && typeof value.use === "function" && typeof value.snapshot === "function");
|
|
326
|
+
return Boolean(value && typeof value.use === "function" && typeof value.snapshot === "function" && value.registry);
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
function applySnapshot(signals, browserCache, snapshot = {}) {
|
|
@@ -353,6 +353,24 @@ function attachServerCache(server, cache) {
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
function normalizeEntries(type, entries = {}) {
|
|
357
|
+
if (type !== "signal") {
|
|
358
|
+
return { ...(entries ?? {}) };
|
|
359
|
+
}
|
|
360
|
+
const normalized = {};
|
|
361
|
+
for (const [id, value] of Object.entries(entries ?? {})) {
|
|
362
|
+
normalized[id] = normalizeSignalDeclaration(value);
|
|
363
|
+
}
|
|
364
|
+
return normalized;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function normalizeSignalDeclaration(value) {
|
|
368
|
+
if (value && typeof value === "object" && typeof value.subscribe === "function") {
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
return createSignal(value);
|
|
372
|
+
}
|
|
373
|
+
|
|
356
374
|
function isLocalServerRegistry(server) {
|
|
357
375
|
return typeof server?.registerMany === "function";
|
|
358
376
|
}
|
|
@@ -361,14 +379,16 @@ function shouldStartRouter(routes, options) {
|
|
|
361
379
|
return Boolean(options.routerOptions || options.mode || routes.entries().length > 0);
|
|
362
380
|
}
|
|
363
381
|
|
|
364
|
-
function renderDocument(routeHtml, { signals, browserCache, boundary }) {
|
|
382
|
+
function renderDocument(routeHtml, { signals, browserCache, boundary, attributes }) {
|
|
365
383
|
const snapshot = {
|
|
366
384
|
signals: signals.snapshot(),
|
|
367
385
|
cache: {
|
|
368
386
|
browser: browserCache.snapshot()
|
|
369
387
|
}
|
|
370
388
|
};
|
|
371
|
-
|
|
389
|
+
const boundaryAttr = attributeName(attributes, "async", "boundary");
|
|
390
|
+
const snapshotAttr = attributeName(attributes, "async", "snapshot");
|
|
391
|
+
return `<section ${boundaryAttr}="${escapeAttribute(boundary)}">${routeHtml ?? ""}</section><script type="application/json" ${snapshotAttr}>${escapeScriptJson(snapshot)}</script>`;
|
|
372
392
|
}
|
|
373
393
|
|
|
374
394
|
function escapeAttribute(value) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const defaultPrefixes = Object.freeze({
|
|
2
|
+
async: ["async:"],
|
|
3
|
+
class: ["class:"],
|
|
4
|
+
signal: ["signal:"],
|
|
5
|
+
on: ["on:"]
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export function defineAttributeConfig(config = {}) {
|
|
9
|
+
return normalizeAttributeConfig(config);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeAttributeConfig(config = {}) {
|
|
13
|
+
return {
|
|
14
|
+
async: normalizePrefixes(config.async, defaultPrefixes.async),
|
|
15
|
+
class: normalizePrefixes(config.class, defaultPrefixes.class),
|
|
16
|
+
signal: normalizePrefixes(config.signal, defaultPrefixes.signal),
|
|
17
|
+
on: normalizePrefixes(config.on, defaultPrefixes.on)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function attributeName(attributes, type, name) {
|
|
22
|
+
return normalizeAttributeConfig(attributes)[type][0] + name;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readAttribute(element, attributes, type, name) {
|
|
26
|
+
for (const prefix of normalizeAttributeConfig(attributes)[type]) {
|
|
27
|
+
const attr = `${prefix}${name}`;
|
|
28
|
+
if (element.hasAttribute?.(attr)) {
|
|
29
|
+
return element.getAttribute(attr);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function matchAttribute(name, attributes, type) {
|
|
36
|
+
for (const prefix of normalizeAttributeConfig(attributes)[type]) {
|
|
37
|
+
if (name.startsWith(prefix)) {
|
|
38
|
+
return name.slice(prefix.length);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizePrefixes(value, fallback) {
|
|
45
|
+
const prefixes = value == null ? fallback : Array.isArray(value) ? value : [value];
|
|
46
|
+
return prefixes.map((prefix) => {
|
|
47
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
48
|
+
throw new TypeError("Attribute prefixes must be non-empty strings.");
|
|
49
|
+
}
|
|
50
|
+
return prefix;
|
|
51
|
+
});
|
|
52
|
+
}
|
package/src/cache.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
2
|
+
|
|
1
3
|
const cacheDefinitionKind = Symbol.for("@async/framework.cacheDefinition");
|
|
2
4
|
|
|
3
5
|
export function defineCache(options = {}) {
|
|
@@ -9,11 +11,12 @@ export function defineCache(options = {}) {
|
|
|
9
11
|
};
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function createCacheRegistry(initialMap = {}, { now = () => Date.now() } = {}) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
14
|
+
export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), registry, type = "cache.browser" } = {}) {
|
|
15
|
+
const registryStore = registry ?? createRegistryStore();
|
|
16
|
+
const definitions = registryStore._map(type);
|
|
17
|
+
const entries = registryStore._map(`${type}.entries`);
|
|
15
18
|
|
|
16
|
-
const
|
|
19
|
+
const registryApi = attachRegistryInspection({
|
|
17
20
|
register(id, definition = defineCache()) {
|
|
18
21
|
assertId(id);
|
|
19
22
|
const normalized = normalizeDefinition(definition);
|
|
@@ -26,9 +29,9 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now() }
|
|
|
26
29
|
|
|
27
30
|
registerMany(map) {
|
|
28
31
|
for (const [id, definition] of Object.entries(map ?? {})) {
|
|
29
|
-
|
|
32
|
+
registryApi.register(id, definition);
|
|
30
33
|
}
|
|
31
|
-
return
|
|
34
|
+
return registryApi;
|
|
32
35
|
},
|
|
33
36
|
|
|
34
37
|
resolve(id) {
|
|
@@ -64,12 +67,12 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now() }
|
|
|
64
67
|
if (typeof fn !== "function") {
|
|
65
68
|
throw new TypeError("cache.getOrSet(key, fn) requires a function.");
|
|
66
69
|
}
|
|
67
|
-
const cached =
|
|
70
|
+
const cached = registryApi.get(key);
|
|
68
71
|
if (cached !== undefined) {
|
|
69
72
|
return cached;
|
|
70
73
|
}
|
|
71
74
|
const value = await fn();
|
|
72
|
-
|
|
75
|
+
registryApi.set(key, value, options);
|
|
73
76
|
return value;
|
|
74
77
|
},
|
|
75
78
|
|
|
@@ -81,20 +84,20 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now() }
|
|
|
81
84
|
clear(prefix) {
|
|
82
85
|
if (prefix === undefined) {
|
|
83
86
|
entries.clear();
|
|
84
|
-
return
|
|
87
|
+
return registryApi;
|
|
85
88
|
}
|
|
86
89
|
for (const key of [...entries.keys()]) {
|
|
87
90
|
if (key.startsWith(prefix)) {
|
|
88
91
|
entries.delete(key);
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
|
-
return
|
|
94
|
+
return registryApi;
|
|
92
95
|
},
|
|
93
96
|
|
|
94
97
|
snapshot() {
|
|
95
98
|
const snapshot = {};
|
|
96
99
|
for (const [key] of entries) {
|
|
97
|
-
const value =
|
|
100
|
+
const value = registryApi.get(key);
|
|
98
101
|
if (value !== undefined) {
|
|
99
102
|
snapshot[key] = value;
|
|
100
103
|
}
|
|
@@ -104,14 +107,26 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now() }
|
|
|
104
107
|
|
|
105
108
|
restore(snapshot = {}) {
|
|
106
109
|
for (const [key, value] of Object.entries(snapshot ?? {})) {
|
|
107
|
-
|
|
110
|
+
registryApi.set(key, value);
|
|
108
111
|
}
|
|
109
|
-
return
|
|
112
|
+
return registryApi;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
entryKeys() {
|
|
116
|
+
return [...entries.keys()];
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
entryEntries() {
|
|
120
|
+
return registryStore.entries(`${type}.entries`);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
_adoptMany() {
|
|
124
|
+
return registryApi;
|
|
110
125
|
}
|
|
111
|
-
};
|
|
126
|
+
}, registryStore, type);
|
|
112
127
|
|
|
113
|
-
|
|
114
|
-
return
|
|
128
|
+
registryApi.registerMany(initialMap);
|
|
129
|
+
return registryApi;
|
|
115
130
|
|
|
116
131
|
function resolvePolicy(key, explicitId) {
|
|
117
132
|
if (explicitId !== undefined) {
|
package/src/component.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { rawHtml, renderTemplate } from "./html.js";
|
|
2
|
+
import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
|
|
2
3
|
|
|
3
4
|
const componentKind = Symbol.for("@async/framework.component");
|
|
4
5
|
let componentCounter = 0;
|
|
@@ -16,10 +17,12 @@ export function defineComponent(fn) {
|
|
|
16
17
|
|
|
17
18
|
export const component = defineComponent;
|
|
18
19
|
|
|
19
|
-
export function createComponentRegistry(initialMap = {}) {
|
|
20
|
-
const
|
|
20
|
+
export function createComponentRegistry(initialMap = {}, options = {}) {
|
|
21
|
+
const registryStore = options.registry ?? createRegistryStore();
|
|
22
|
+
const type = options.type ?? "component";
|
|
23
|
+
const entries = registryStore._map(type);
|
|
21
24
|
|
|
22
|
-
const registry = {
|
|
25
|
+
const registry = attachRegistryInspection({
|
|
23
26
|
register(id, Component) {
|
|
24
27
|
if (typeof id !== "string" || id.length === 0) {
|
|
25
28
|
throw new TypeError("Component id must be a non-empty string.");
|
|
@@ -46,8 +49,12 @@ export function createComponentRegistry(initialMap = {}) {
|
|
|
46
49
|
throw new TypeError("Component id must be a non-empty string.");
|
|
47
50
|
}
|
|
48
51
|
return entries.get(id);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
_adoptMany() {
|
|
55
|
+
return registry;
|
|
49
56
|
}
|
|
50
|
-
};
|
|
57
|
+
}, registryStore, type);
|
|
51
58
|
|
|
52
59
|
registry.registerMany(initialMap);
|
|
53
60
|
return registry;
|
|
@@ -64,29 +71,46 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
64
71
|
|
|
65
72
|
const scope = `${parentScope}.${componentName(Component)}.${++componentCounter}`;
|
|
66
73
|
const cleanups = [];
|
|
67
|
-
const
|
|
74
|
+
const attachHooks = [];
|
|
68
75
|
const visibleHooks = [];
|
|
76
|
+
const destroyHooks = [];
|
|
77
|
+
const bindingIds = [];
|
|
69
78
|
const context = createComponentContext({
|
|
70
79
|
runtime,
|
|
71
80
|
scope,
|
|
72
81
|
cleanups,
|
|
73
|
-
|
|
74
|
-
visibleHooks
|
|
82
|
+
attachHooks,
|
|
83
|
+
visibleHooks,
|
|
84
|
+
destroyHooks
|
|
75
85
|
});
|
|
76
86
|
|
|
77
87
|
const output = Component.call(context, props);
|
|
78
|
-
const html = renderTemplate(output
|
|
88
|
+
const html = renderTemplate(output, {
|
|
89
|
+
attributes: runtime.attributes,
|
|
90
|
+
signals: runtime.signals,
|
|
91
|
+
bind(value) {
|
|
92
|
+
const id = runtime.loader?._registerBinding?.(value);
|
|
93
|
+
if (!id) {
|
|
94
|
+
throw new Error("Inline template bindings require an AsyncLoader.");
|
|
95
|
+
}
|
|
96
|
+
bindingIds.push(id);
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
79
100
|
|
|
80
101
|
return {
|
|
81
102
|
html,
|
|
82
|
-
|
|
83
|
-
for (const hook of
|
|
103
|
+
attach(target) {
|
|
104
|
+
for (const hook of attachHooks) {
|
|
84
105
|
const cleanup = hook(target);
|
|
85
106
|
if (typeof cleanup === "function") {
|
|
86
107
|
cleanups.push(cleanup);
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
},
|
|
111
|
+
mount(target) {
|
|
112
|
+
this.attach(target);
|
|
113
|
+
},
|
|
90
114
|
visible(target, observeVisible) {
|
|
91
115
|
for (const hook of visibleHooks) {
|
|
92
116
|
const cleanup = observeVisible(target, hook);
|
|
@@ -96,15 +120,24 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
|
|
|
96
120
|
}
|
|
97
121
|
},
|
|
98
122
|
cleanup() {
|
|
123
|
+
while (destroyHooks.length > 0) {
|
|
124
|
+
destroyHooks.pop()?.();
|
|
125
|
+
}
|
|
99
126
|
while (cleanups.length > 0) {
|
|
100
127
|
cleanups.pop()?.();
|
|
101
128
|
}
|
|
129
|
+
while (bindingIds.length > 0) {
|
|
130
|
+
runtime.loader?._releaseBinding?.(bindingIds.pop());
|
|
131
|
+
}
|
|
102
132
|
}
|
|
103
133
|
};
|
|
104
134
|
}
|
|
105
135
|
|
|
106
|
-
function createComponentContext({ runtime, scope, cleanups,
|
|
136
|
+
function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks }) {
|
|
107
137
|
const { signals, handlers, loader, server, router, cache } = runtime;
|
|
138
|
+
const generatedHandlers = new WeakMap();
|
|
139
|
+
let generatedHandlerCounter = 0;
|
|
140
|
+
let generatedSignalCounter = 0;
|
|
108
141
|
const context = {
|
|
109
142
|
scope,
|
|
110
143
|
signals,
|
|
@@ -115,6 +148,9 @@ function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleH
|
|
|
115
148
|
cache,
|
|
116
149
|
|
|
117
150
|
signal(name, initial) {
|
|
151
|
+
if (arguments.length === 1) {
|
|
152
|
+
return signals.ensure(scoped(scope, `signal.${++generatedSignalCounter}`), name);
|
|
153
|
+
}
|
|
118
154
|
return signals.ensure(scoped(scope, name), initial);
|
|
119
155
|
},
|
|
120
156
|
|
|
@@ -143,31 +179,70 @@ function createComponentContext({ runtime, scope, cleanups, mountHooks, visibleH
|
|
|
143
179
|
},
|
|
144
180
|
|
|
145
181
|
handler(name, fn) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
182
|
+
if (typeof name === "function" && fn === undefined) {
|
|
183
|
+
const inlineFn = name;
|
|
184
|
+
if (generatedHandlers.has(inlineFn)) {
|
|
185
|
+
return generatedHandlers.get(inlineFn);
|
|
186
|
+
}
|
|
187
|
+
const id = registerScopedHandler(`handler.${++generatedHandlerCounter}`, inlineFn);
|
|
188
|
+
generatedHandlers.set(inlineFn, id);
|
|
189
|
+
return id;
|
|
190
|
+
}
|
|
191
|
+
if (typeof fn !== "function") {
|
|
192
|
+
throw new TypeError("this.handler(name, fn) or this.handler(fn) requires a function.");
|
|
193
|
+
}
|
|
194
|
+
return registerScopedHandler(name, fn);
|
|
151
195
|
},
|
|
152
196
|
|
|
153
197
|
render(Child, childProps = {}) {
|
|
154
198
|
const child = renderComponent(Child, childProps, runtime, scope);
|
|
155
199
|
cleanups.push(child.cleanup);
|
|
156
|
-
|
|
200
|
+
attachHooks.push((target) => child.attach(target));
|
|
157
201
|
visibleHooks.push((target) => child.visible(target, loader._observeVisible));
|
|
158
202
|
return rawHtml(child.html);
|
|
159
203
|
},
|
|
160
204
|
|
|
205
|
+
on(eventName, fn) {
|
|
206
|
+
if (typeof eventName !== "string" || eventName.length === 0) {
|
|
207
|
+
throw new TypeError("Component lifecycle event must be a non-empty string.");
|
|
208
|
+
}
|
|
209
|
+
if (typeof fn !== "function") {
|
|
210
|
+
throw new TypeError(`Component lifecycle "${eventName}" requires a function.`);
|
|
211
|
+
}
|
|
212
|
+
const event = eventName === "mount" ? "attach" : eventName;
|
|
213
|
+
if (event === "attach") {
|
|
214
|
+
attachHooks.push((target) => fn.call(context, target));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (event === "visible") {
|
|
218
|
+
visibleHooks.push((target) => fn.call(context, target));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (event === "destroy") {
|
|
222
|
+
destroyHooks.push(() => fn.call(context));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`Unsupported component lifecycle event "${eventName}".`);
|
|
226
|
+
},
|
|
227
|
+
|
|
161
228
|
onMount(fn) {
|
|
162
|
-
|
|
229
|
+
context.on("attach", fn);
|
|
163
230
|
},
|
|
164
231
|
|
|
165
232
|
onVisible(fn) {
|
|
166
|
-
|
|
233
|
+
context.on("visible", fn);
|
|
167
234
|
}
|
|
168
235
|
};
|
|
169
236
|
|
|
170
237
|
return context;
|
|
238
|
+
|
|
239
|
+
function registerScopedHandler(name, fn) {
|
|
240
|
+
const id = scoped(scope, name);
|
|
241
|
+
handlers.register(id, function runComponentHandler(handlerContext) {
|
|
242
|
+
return fn.call({ ...context, ...handlerContext }, handlerContext);
|
|
243
|
+
});
|
|
244
|
+
return id;
|
|
245
|
+
}
|
|
171
246
|
}
|
|
172
247
|
|
|
173
248
|
function scoped(scope, name) {
|