@async/framework 0.10.2 → 0.11.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +23 -7
  3. package/browser.d.ts +4 -7
  4. package/browser.js +17 -66
  5. package/browser.min.js +1 -1
  6. package/browser.ts +17 -66
  7. package/browser.umd.js +17 -66
  8. package/browser.umd.min.js +1 -1
  9. package/{server.d.ts → framework.d.ts} +4 -7
  10. package/framework.ts +5946 -0
  11. package/package.json +25 -17
  12. package/server.js +5945 -0
  13. package/examples/cache/index.html +0 -16
  14. package/examples/cache/main.js +0 -47
  15. package/examples/components/index.html +0 -11
  16. package/examples/components/main.js +0 -26
  17. package/examples/counter/index.html +0 -15
  18. package/examples/counter/main.js +0 -17
  19. package/examples/partials/index.html +0 -15
  20. package/examples/partials/main.js +0 -43
  21. package/examples/product/index.html +0 -32
  22. package/examples/product/main.js +0 -24
  23. package/examples/router/index.html +0 -18
  24. package/examples/router/main.js +0 -52
  25. package/examples/server-call/index.html +0 -21
  26. package/examples/server-call/main.js +0 -22
  27. package/examples/ssr/index.html +0 -12
  28. package/examples/ssr/main.js +0 -89
  29. package/examples/streaming/index.html +0 -16
  30. package/examples/streaming/main.js +0 -30
  31. package/src/app.js +0 -802
  32. package/src/async-signal.js +0 -277
  33. package/src/attributes.js +0 -52
  34. package/src/boundary-receiver.js +0 -302
  35. package/src/browser.js +0 -18
  36. package/src/cache.js +0 -193
  37. package/src/component.js +0 -373
  38. package/src/delay.js +0 -30
  39. package/src/elements.js +0 -63
  40. package/src/handlers.js +0 -219
  41. package/src/html.js +0 -158
  42. package/src/index.js +0 -20
  43. package/src/lazy-registry.js +0 -218
  44. package/src/loader.js +0 -772
  45. package/src/partials.js +0 -133
  46. package/src/registry-store.js +0 -267
  47. package/src/request-context.js +0 -40
  48. package/src/router.js +0 -617
  49. package/src/scheduler.js +0 -300
  50. package/src/server-entry.js +0 -20
  51. package/src/server-registry.js +0 -97
  52. package/src/server.js +0 -362
  53. package/src/signals.js +0 -592
package/src/cache.js DELETED
@@ -1,193 +0,0 @@
1
- import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
2
-
3
- const cacheDefinitionKind = Symbol.for("@async/framework.cacheDefinition");
4
-
5
- export function defineCache(options = {}) {
6
- return {
7
- [cacheDefinitionKind]: true,
8
- kind: "cache-definition",
9
- store: options.store ?? "memory",
10
- ttl: options.ttl
11
- };
12
- }
13
-
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`);
18
- const pending = new Map();
19
-
20
- const registryApi = attachRegistryInspection({
21
- register(id, definition = defineCache()) {
22
- assertId(id);
23
- const normalized = normalizeDefinition(definition);
24
- if (definitions.has(id)) {
25
- throw new Error(`Cache "${id}" is already registered.`);
26
- }
27
- definitions.set(id, normalized);
28
- return id;
29
- },
30
-
31
- registerMany(map) {
32
- for (const [id, definition] of Object.entries(map ?? {})) {
33
- registryApi.register(id, definition);
34
- }
35
- return registryApi;
36
- },
37
-
38
- unregister(id) {
39
- assertId(id);
40
- return definitions.delete(id);
41
- },
42
-
43
- resolve(id) {
44
- assertId(id);
45
- return definitions.get(id);
46
- },
47
-
48
- get(key) {
49
- assertKey(key);
50
- return readEntry(key).value;
51
- },
52
-
53
- set(key, value, options = {}) {
54
- assertKey(key);
55
- const ttl = options.ttl ?? resolvePolicy(key, options.cache)?.ttl;
56
- entries.set(key, {
57
- value,
58
- expiresAt: ttl === undefined ? undefined : now() + ttl
59
- });
60
- return value;
61
- },
62
-
63
- async getOrSet(key, fn, options = {}) {
64
- assertKey(key);
65
- if (typeof fn !== "function") {
66
- throw new TypeError("cache.getOrSet(key, fn) requires a function.");
67
- }
68
- const cached = readEntry(key);
69
- if (cached.found) {
70
- return cached.value;
71
- }
72
- if (pending.has(key)) {
73
- return pending.get(key);
74
- }
75
- let promise;
76
- promise = Promise.resolve()
77
- .then(fn)
78
- .then((value) => {
79
- if (pending.get(key) === promise) {
80
- registryApi.set(key, value, options);
81
- }
82
- return value;
83
- })
84
- .finally(() => {
85
- if (pending.get(key) === promise) {
86
- pending.delete(key);
87
- }
88
- });
89
- pending.set(key, promise);
90
- return promise;
91
- },
92
-
93
- delete(key) {
94
- assertKey(key);
95
- pending.delete(key);
96
- return entries.delete(key);
97
- },
98
-
99
- clear(prefix) {
100
- if (prefix === undefined) {
101
- entries.clear();
102
- pending.clear();
103
- return registryApi;
104
- }
105
- for (const key of [...entries.keys()]) {
106
- if (key.startsWith(prefix)) {
107
- entries.delete(key);
108
- }
109
- }
110
- for (const key of [...pending.keys()]) {
111
- if (key.startsWith(prefix)) {
112
- pending.delete(key);
113
- }
114
- }
115
- return registryApi;
116
- },
117
-
118
- snapshot() {
119
- const snapshot = {};
120
- for (const [key] of entries) {
121
- const { found, value } = readEntry(key);
122
- if (found && value !== undefined) {
123
- snapshot[key] = value;
124
- }
125
- }
126
- return snapshot;
127
- },
128
-
129
- restore(snapshot = {}) {
130
- for (const [key, value] of Object.entries(snapshot ?? {})) {
131
- registryApi.set(key, value);
132
- }
133
- return registryApi;
134
- },
135
-
136
- entryKeys() {
137
- return [...entries.keys()];
138
- },
139
-
140
- entryEntries() {
141
- return registryStore.entries(`${type}.entries`);
142
- },
143
-
144
- _adoptMany() {
145
- return registryApi;
146
- }
147
- }, registryStore, type);
148
-
149
- registryApi.registerMany(initialMap);
150
- return registryApi;
151
-
152
- function resolvePolicy(key, explicitId) {
153
- if (explicitId !== undefined) {
154
- return definitions.get(explicitId);
155
- }
156
- if (definitions.has(key)) {
157
- return definitions.get(key);
158
- }
159
- const prefix = key.split(":")[0];
160
- return definitions.get(prefix);
161
- }
162
-
163
- function readEntry(key) {
164
- const entry = entries.get(key);
165
- if (!entry) {
166
- return { found: false, value: undefined };
167
- }
168
- if (entry.expiresAt !== undefined && entry.expiresAt <= now()) {
169
- entries.delete(key);
170
- return { found: false, value: undefined };
171
- }
172
- return { found: true, value: entry.value };
173
- }
174
- }
175
-
176
- function normalizeDefinition(definition) {
177
- if (definition?.[cacheDefinitionKind]) {
178
- return definition;
179
- }
180
- return defineCache(definition);
181
- }
182
-
183
- function assertId(id) {
184
- if (typeof id !== "string" || id.length === 0) {
185
- throw new TypeError("Cache id must be a non-empty string.");
186
- }
187
- }
188
-
189
- function assertKey(key) {
190
- if (typeof key !== "string" || key.length === 0) {
191
- throw new TypeError("Cache key must be a non-empty string.");
192
- }
193
- }
package/src/component.js DELETED
@@ -1,373 +0,0 @@
1
- import { attributeName } from "./attributes.js";
2
- import { escapeHtml, rawHtml, renderTemplate } from "./html.js";
3
- import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
4
- import { createLazyRegistry, isLazyDescriptor } from "./lazy-registry.js";
5
-
6
- const componentKind = Symbol.for("@async/framework.component");
7
- let componentCounter = 0;
8
-
9
- export function defineComponent(fn) {
10
- if (typeof fn !== "function") {
11
- throw new TypeError("defineComponent(fn) requires a function.");
12
- }
13
- Object.defineProperty(fn, componentKind, {
14
- configurable: true,
15
- value: true
16
- });
17
- return fn;
18
- }
19
-
20
- export const component = defineComponent;
21
-
22
- export function createComponentRegistry(initialMap = {}, options = {}) {
23
- const registryStore = options.registry ?? createRegistryStore();
24
- const type = options.type ?? "component";
25
- const entries = registryStore._map(type);
26
- const lazyRegistry = options.lazyRegistry ?? createLazyRegistry(options);
27
- const lazyComponents = new Map();
28
-
29
- const registry = attachRegistryInspection({
30
- register(id, Component) {
31
- if (typeof id !== "string" || id.length === 0) {
32
- throw new TypeError("Component id must be a non-empty string.");
33
- }
34
- if (!isComponent(Component) && typeof Component !== "function" && !isLazyDescriptor(Component)) {
35
- throw new TypeError(`Component "${id}" must be a component function.`);
36
- }
37
- if (entries.has(id)) {
38
- throw new Error(`Component "${id}" is already registered.`);
39
- }
40
- entries.set(id, Component);
41
- return id;
42
- },
43
-
44
- registerMany(map) {
45
- for (const [id, Component] of Object.entries(map ?? {})) {
46
- registry.register(id, Component);
47
- }
48
- return registry;
49
- },
50
-
51
- unregister(id) {
52
- if (typeof id !== "string" || id.length === 0) {
53
- throw new TypeError("Component id must be a non-empty string.");
54
- }
55
- lazyComponents.delete(id);
56
- return entries.delete(id);
57
- },
58
-
59
- resolve(id) {
60
- if (typeof id !== "string" || id.length === 0) {
61
- throw new TypeError("Component id must be a non-empty string.");
62
- }
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);
77
- },
78
-
79
- _adoptMany() {
80
- return registry;
81
- }
82
- }, registryStore, type);
83
-
84
- registry.registerMany(initialMap);
85
- return registry;
86
- }
87
-
88
- export function isComponent(value) {
89
- return Boolean(value?.[componentKind]);
90
- }
91
-
92
- export function renderComponent(Component, props = {}, runtime, parentScope = "component") {
93
- if (!isComponent(Component) && typeof Component !== "function") {
94
- throw new TypeError("renderComponent(Component) requires a component function.");
95
- }
96
-
97
- const scope = `${parentScope}.${componentName(Component)}.${++componentCounter}`;
98
- const cleanups = [];
99
- const attachHooks = [];
100
- const visibleHooks = [];
101
- const destroyHooks = [];
102
- const bindingIds = [];
103
- const templateOptions = {
104
- attributes: runtime.attributes,
105
- signals: runtime.signals,
106
- bind(value) {
107
- const id = runtime.loader?._registerBinding?.(value);
108
- if (!id) {
109
- throw new Error("Inline template bindings require a Loader.");
110
- }
111
- bindingIds.push(id);
112
- return id;
113
- }
114
- };
115
- const renderScopedTemplate = (value) => renderTemplate(value, templateOptions);
116
- const context = createComponentContext({
117
- runtime,
118
- scope,
119
- cleanups,
120
- attachHooks,
121
- visibleHooks,
122
- destroyHooks,
123
- renderScopedTemplate
124
- });
125
-
126
- const output = Component.call(context, props);
127
- const html = renderScopedTemplate(output);
128
-
129
- return {
130
- html,
131
- attach(target) {
132
- for (const hook of attachHooks) {
133
- runtime.scheduler?.enqueue("lifecycle", () => {
134
- const cleanup = hook(target);
135
- if (typeof cleanup === "function") {
136
- cleanups.push(cleanup);
137
- }
138
- }, {
139
- scope,
140
- key: `attach:${attachHooks.indexOf(hook)}`
141
- }) ?? runAttachHook(hook, target);
142
- }
143
- },
144
- mount(target) {
145
- this.attach(target);
146
- },
147
- visible(target, observeVisible) {
148
- for (const hook of visibleHooks) {
149
- const cleanup = observeVisible(target, () => {
150
- runtime.scheduler?.enqueue("lifecycle", () => {
151
- const hookCleanup = hook(target);
152
- if (typeof hookCleanup === "function") {
153
- cleanups.push(hookCleanup);
154
- }
155
- }, {
156
- scope,
157
- key: `visible:${visibleHooks.indexOf(hook)}`
158
- }) ?? runVisibleHook(hook, target);
159
- });
160
- if (typeof cleanup === "function") {
161
- cleanups.push(cleanup);
162
- }
163
- }
164
- },
165
- cleanup() {
166
- while (destroyHooks.length > 0) {
167
- destroyHooks.pop()?.();
168
- }
169
- runtime.scheduler?.markScopeDestroyed(scope);
170
- while (cleanups.length > 0) {
171
- cleanups.pop()?.();
172
- }
173
- while (bindingIds.length > 0) {
174
- runtime.loader?._releaseBinding?.(bindingIds.pop());
175
- }
176
- }
177
- };
178
-
179
- function runAttachHook(hook, target) {
180
- const cleanup = hook(target);
181
- if (typeof cleanup === "function") {
182
- cleanups.push(cleanup);
183
- }
184
- }
185
-
186
- function runVisibleHook(hook, target) {
187
- const cleanup = hook(target);
188
- if (typeof cleanup === "function") {
189
- cleanups.push(cleanup);
190
- }
191
- }
192
- }
193
-
194
- function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
195
- const { signals, handlers, loader, server, router, cache, scheduler } = runtime;
196
- const generatedHandlers = new WeakMap();
197
- let generatedHandlerCounter = 0;
198
- let generatedSignalCounter = 0;
199
- const context = {
200
- scope,
201
- signals,
202
- handlers,
203
- loader,
204
- server,
205
- router,
206
- cache,
207
- scheduler,
208
-
209
- signal(name, initial) {
210
- if (arguments.length === 1) {
211
- const id = scoped(scope, `signal.${++generatedSignalCounter}`);
212
- const ref = signals.ensure(id, name);
213
- cleanups.push(() => signals.unregister?.(id));
214
- return ref;
215
- }
216
- const id = scoped(scope, name);
217
- const created = !signals.has(id);
218
- const ref = signals.ensure(id, initial);
219
- if (created) {
220
- cleanups.push(() => signals.unregister?.(id));
221
- }
222
- return ref;
223
- },
224
-
225
- computed(name, fn) {
226
- const id = scoped(scope, name);
227
- const created = !signals.has(id);
228
- const ref = signals.ensure(id, undefined);
229
- if (created) {
230
- cleanups.push(() => signals.unregister?.(id));
231
- }
232
- const cleanup = signals.effect(() => {
233
- signals.set(id, fn.call(context));
234
- });
235
- cleanups.push(cleanup);
236
- return ref;
237
- },
238
-
239
- asyncSignal(name, fn) {
240
- const id = scoped(scope, name);
241
- const created = !signals.has(id);
242
- if (!signals.has(id)) {
243
- signals.asyncSignal(id, fn);
244
- }
245
- if (created) {
246
- cleanups.push(() => signals.unregister?.(id));
247
- }
248
- return signals.ref(id);
249
- },
250
-
251
- effect(fn) {
252
- const cleanup = signals.effect(() => fn.call(context), {
253
- scheduler,
254
- phase: "effect",
255
- scope
256
- });
257
- cleanups.push(cleanup);
258
- return cleanup;
259
- },
260
-
261
- handler(name, fn) {
262
- if (typeof name === "function" && fn === undefined) {
263
- const inlineFn = name;
264
- if (generatedHandlers.has(inlineFn)) {
265
- return generatedHandlers.get(inlineFn);
266
- }
267
- const id = registerScopedHandler(`handler.${++generatedHandlerCounter}`, inlineFn);
268
- generatedHandlers.set(inlineFn, id);
269
- return id;
270
- }
271
- if (typeof fn !== "function") {
272
- throw new TypeError("this.handler(name, fn) or this.handler(fn) requires a function.");
273
- }
274
- return registerScopedHandler(name, fn);
275
- },
276
-
277
- render(Child, childProps = {}) {
278
- const child = renderComponent(Child, childProps, runtime, scope);
279
- cleanups.push(child.cleanup);
280
- attachHooks.push((target) => child.attach(target));
281
- visibleHooks.push((target) => child.visible(target, loader._observeVisible));
282
- return rawHtml(child.html);
283
- },
284
-
285
- suspense(signalRef, views) {
286
- const id = signalRef?.id;
287
- if (!id) {
288
- throw new TypeError("this.suspense(signalRef, views) requires a signal ref.");
289
- }
290
-
291
- const normalized = normalizeSuspenseViews(views);
292
- const chunks = [];
293
- for (const state of ["loading", "ready", "error"]) {
294
- const view = normalized[state];
295
- if (!view) {
296
- continue;
297
- }
298
- const attr = attributeName(runtime.attributes, "async", state);
299
- const body = renderScopedTemplate(view.call(context, signalRef));
300
- chunks.push(`<template ${attr}="${escapeHtml(id)}">${body}</template>`);
301
- }
302
- return rawHtml(chunks.join(""));
303
- },
304
-
305
- on(eventName, fn) {
306
- if (typeof eventName !== "string" || eventName.length === 0) {
307
- throw new TypeError("Component lifecycle event must be a non-empty string.");
308
- }
309
- if (typeof fn !== "function") {
310
- throw new TypeError(`Component lifecycle "${eventName}" requires a function.`);
311
- }
312
- const event = eventName === "mount" ? "attach" : eventName;
313
- if (event === "attach") {
314
- attachHooks.push((target) => fn.call(context, target));
315
- return;
316
- }
317
- if (event === "visible") {
318
- visibleHooks.push((target) => fn.call(context, target));
319
- return;
320
- }
321
- if (event === "destroy") {
322
- destroyHooks.push(() => fn.call(context));
323
- return;
324
- }
325
- throw new Error(`Unsupported component lifecycle event "${eventName}".`);
326
- },
327
-
328
- onMount(fn) {
329
- context.on("attach", fn);
330
- },
331
-
332
- onVisible(fn) {
333
- context.on("visible", fn);
334
- }
335
- };
336
-
337
- return context;
338
-
339
- function registerScopedHandler(name, fn) {
340
- const id = scoped(scope, name);
341
- handlers.register(id, function runComponentHandler(handlerContext) {
342
- return fn.call({ ...context, ...handlerContext }, handlerContext);
343
- });
344
- cleanups.push(() => handlers.unregister?.(id));
345
- return id;
346
- }
347
- }
348
-
349
- function scoped(scope, name) {
350
- if (typeof name !== "string" || name.length === 0) {
351
- throw new TypeError("Scoped signal or handler name must be a non-empty string.");
352
- }
353
- return `${scope}.${name}`;
354
- }
355
-
356
- function normalizeSuspenseViews(views) {
357
- const normalized = typeof views === "function" ? { ready: views } : views;
358
- if (!normalized || typeof normalized !== "object" || Array.isArray(normalized)) {
359
- throw new TypeError("this.suspense(signalRef, views) requires views to be a function or object.");
360
- }
361
-
362
- for (const state of ["loading", "ready", "error"]) {
363
- if (Object.hasOwn(normalized, state) && normalized[state] !== undefined && typeof normalized[state] !== "function") {
364
- throw new TypeError(`this.suspense(signalRef, views) view "${state}" must be a function.`);
365
- }
366
- }
367
-
368
- return normalized;
369
- }
370
-
371
- function componentName(Component) {
372
- return Component.displayName || Component.name || "anonymous";
373
- }
package/src/delay.js DELETED
@@ -1,30 +0,0 @@
1
- export function delay(ms, signal) {
2
- if (signal?.aborted) {
3
- return Promise.reject(abortReason(signal));
4
- }
5
-
6
- return new Promise((resolve, reject) => {
7
- let timer = setTimeout(done, ms);
8
-
9
- function done() {
10
- timer = undefined;
11
- signal?.removeEventListener?.("abort", aborted);
12
- resolve();
13
- }
14
-
15
- function aborted() {
16
- if (timer !== undefined) {
17
- clearTimeout(timer);
18
- }
19
- timer = undefined;
20
- signal?.removeEventListener?.("abort", aborted);
21
- reject(abortReason(signal));
22
- }
23
-
24
- signal?.addEventListener?.("abort", aborted, { once: true });
25
- });
26
- }
27
-
28
- function abortReason(signal) {
29
- return signal?.reason ?? new Error("Operation aborted");
30
- }
package/src/elements.js DELETED
@@ -1,63 +0,0 @@
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
- }