@async/framework 0.2.2 → 0.3.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/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 declarations = emptyDeclarations();
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(declarations, normalized);
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 cloneDeclarations(declarations);
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 snapshot = app.snapshot();
57
- const signals = options.signals ?? createSignalRegistry(snapshot.signal);
58
- const handlers = options.handlers ?? createHandlerRegistry(snapshot.handler);
59
- const serverCache = createCacheRegistry(snapshot.cache.server);
60
- const browserCache = createCacheRegistry(snapshot.cache.browser);
61
- const server = options.server ?? createServerRegistry(snapshot.server);
62
- const partials = options.partials ?? createPartialRegistry(snapshot.partial);
63
- const routes = options.routes ?? createRouteRegistry(snapshot.route);
64
- const components = options.components ?? createComponentRegistry(snapshot.component);
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.registerMany(normalized.signal);
237
- runtime.handlers.registerMany(normalized.handler);
238
- if (typeof runtime.server.registerMany === "function") {
239
- runtime.server.registerMany(normalized.server);
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
- runtime.partials.registerMany(normalized.partial);
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] = { ...(entries ?? {}) };
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] = { ...(value ?? {}) };
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[type], source[type], type);
313
+ addEntries(target, type, source[type]);
296
314
  }
297
- addEntries(target.cache.browser, source.cache.browser, "cache.browser");
298
- addEntries(target.cache.server, source.cache.server, "cache.server");
315
+ addEntries(target, "cache.browser", source.cache.browser);
316
+ addEntries(target, "cache.server", source.cache.server);
299
317
  }
300
318
 
301
- function addEntries(target, source, label) {
319
+ function addEntries(registry, type, source) {
302
320
  for (const [id, value] of Object.entries(source ?? {})) {
303
- if (Object.hasOwn(target, id)) {
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
- return `<section data-async-boundary="${escapeAttribute(boundary)}">${routeHtml ?? ""}</section><script type="application/json" data-async-snapshot>${escapeScriptJson(snapshot)}</script>`;
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,50 @@
1
+ const defaultPrefixes = Object.freeze({
2
+ async: ["async:"],
3
+ signal: ["signal:"],
4
+ on: ["on:"]
5
+ });
6
+
7
+ export function defineAttributeConfig(config = {}) {
8
+ return normalizeAttributeConfig(config);
9
+ }
10
+
11
+ export function normalizeAttributeConfig(config = {}) {
12
+ return {
13
+ async: normalizePrefixes(config.async, defaultPrefixes.async),
14
+ signal: normalizePrefixes(config.signal, defaultPrefixes.signal),
15
+ on: normalizePrefixes(config.on, defaultPrefixes.on)
16
+ };
17
+ }
18
+
19
+ export function attributeName(attributes, type, name) {
20
+ return normalizeAttributeConfig(attributes)[type][0] + name;
21
+ }
22
+
23
+ export function readAttribute(element, attributes, type, name) {
24
+ for (const prefix of normalizeAttributeConfig(attributes)[type]) {
25
+ const attr = `${prefix}${name}`;
26
+ if (element.hasAttribute?.(attr)) {
27
+ return element.getAttribute(attr);
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+
33
+ export function matchAttribute(name, attributes, type) {
34
+ for (const prefix of normalizeAttributeConfig(attributes)[type]) {
35
+ if (name.startsWith(prefix)) {
36
+ return name.slice(prefix.length);
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function normalizePrefixes(value, fallback) {
43
+ const prefixes = value == null ? fallback : Array.isArray(value) ? value : [value];
44
+ return prefixes.map((prefix) => {
45
+ if (typeof prefix !== "string" || prefix.length === 0) {
46
+ throw new TypeError("Attribute prefixes must be non-empty strings.");
47
+ }
48
+ return prefix;
49
+ });
50
+ }
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 definitions = new Map();
14
- const entries = new Map();
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 registry = {
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
- registry.register(id, definition);
32
+ registryApi.register(id, definition);
30
33
  }
31
- return registry;
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 = registry.get(key);
70
+ const cached = registryApi.get(key);
68
71
  if (cached !== undefined) {
69
72
  return cached;
70
73
  }
71
74
  const value = await fn();
72
- registry.set(key, value, options);
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 registry;
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 registry;
94
+ return registryApi;
92
95
  },
93
96
 
94
97
  snapshot() {
95
98
  const snapshot = {};
96
99
  for (const [key] of entries) {
97
- const value = registry.get(key);
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
- registry.set(key, value);
110
+ registryApi.set(key, value);
108
111
  }
109
- return registry;
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
- registry.registerMany(initialMap);
114
- return registry;
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 entries = new Map();
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;
package/src/handlers.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  resolveServerCommandArguments,
5
5
  unwrapServerResult
6
6
  } from "./server.js";
7
+ import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
7
8
 
8
9
  const builtInTokens = new Set(["preventDefault", "stopPropagation", "stopImmediatePropagation"]);
9
10
  const builtInHandlers = {
@@ -18,10 +19,12 @@ const builtInHandlers = {
18
19
  }
19
20
  };
20
21
 
21
- export function createHandlerRegistry(initialMap = {}) {
22
- const handlers = new Map();
22
+ export function createHandlerRegistry(initialMap = {}, options = {}) {
23
+ const registryStore = options.registry ?? createRegistryStore();
24
+ const type = options.type ?? "handler";
25
+ const handlers = registryStore._map(type);
23
26
 
24
- const registry = {
27
+ const registry = attachRegistryInspection({
25
28
  register(id, fn) {
26
29
  assertId(id);
27
30
  if (typeof fn !== "function") {
@@ -90,14 +93,30 @@ export function createHandlerRegistry(initialMap = {}) {
90
93
  }
91
94
 
92
95
  return results;
96
+ },
97
+
98
+ _adoptMany() {
99
+ return registry;
93
100
  }
94
- };
101
+ }, registryStore, type);
95
102
 
96
- registry.registerMany(builtInHandlers);
103
+ registerBuiltIns(registry, handlers);
97
104
  registry.registerMany(initialMap);
98
105
  return registry;
99
106
  }
100
107
 
108
+ function registerBuiltIns(registry, handlers) {
109
+ for (const [id, fn] of Object.entries(builtInHandlers)) {
110
+ if (!handlers.has(id)) {
111
+ registry.register(id, fn);
112
+ continue;
113
+ }
114
+ if (handlers.get(id) !== fn) {
115
+ throw new Error(`Handler "${id}" is already registered.`);
116
+ }
117
+ }
118
+ }
119
+
101
120
  export function parseHandlerRef(ref) {
102
121
  if (typeof ref !== "string" || ref.trim().length === 0) {
103
122
  throw new TypeError("Handler ref must be a non-empty string.");
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { asyncSignal } from "./async-signal.js";
2
2
  export { Async, createApp, defineApp } from "./app.js";
3
+ export { attributeName, defineAttributeConfig } from "./attributes.js";
3
4
  export { createCacheRegistry, defineCache } from "./cache.js";
4
5
  export { component, createComponentRegistry, defineComponent } from "./component.js";
5
6
  export { delay } from "./delay.js";
@@ -7,6 +8,7 @@ export { createHandlerRegistry } from "./handlers.js";
7
8
  export { html } from "./html.js";
8
9
  export { AsyncLoader } from "./loader.js";
9
10
  export { createPartialRegistry } from "./partials.js";
11
+ export { createRegistryStore } from "./registry-store.js";
10
12
  export { createRouteRegistry, createRouter, defineRoute, route } from "./router.js";
11
13
  export { createServerProxy, createServerRegistry } from "./server.js";
12
14
  export { computed, createSignal, createSignalRegistry, effect, signal } from "./signals.js";