@async/framework 0.10.1 → 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 +36 -0
  2. package/README.md +23 -7
  3. package/browser.d.ts +4 -7
  4. package/browser.js +143 -116
  5. package/browser.min.js +1 -1
  6. package/browser.ts +143 -116
  7. package/browser.umd.js +143 -116
  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 -189
  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 -204
  44. package/src/loader.js +0 -765
  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 -571
  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 -357
  53. package/src/signals.js +0 -592
@@ -1,277 +0,0 @@
1
- const asyncSignalKind = Symbol.for("@async/framework.asyncSignal");
2
-
3
- export function asyncSignal(id, fn) {
4
- if (typeof id !== "string" || id.length === 0) {
5
- throw new TypeError("asyncSignal(id, fn) requires a non-empty string id.");
6
- }
7
- if (typeof fn !== "function") {
8
- throw new TypeError("asyncSignal(id, fn) requires a function.");
9
- }
10
-
11
- let value;
12
- let loading = false;
13
- let error = null;
14
- let status = "idle";
15
- let version = 0;
16
- let registry;
17
- let registeredId = id;
18
- let activeController;
19
- let activeAbort;
20
- const subscribers = new Set();
21
- const dependencyCleanups = new Set();
22
-
23
- const state = {
24
- [asyncSignalKind]: true,
25
- kind: "async-signal",
26
-
27
- get id() {
28
- return registeredId;
29
- },
30
-
31
- get value() {
32
- return value;
33
- },
34
-
35
- get loading() {
36
- return loading;
37
- },
38
-
39
- get error() {
40
- return error;
41
- },
42
-
43
- get status() {
44
- return status;
45
- },
46
-
47
- get version() {
48
- return version;
49
- },
50
-
51
- set(nextValue) {
52
- value = nextValue;
53
- loading = false;
54
- error = null;
55
- status = "ready";
56
- notify();
57
- return value;
58
- },
59
-
60
- refresh() {
61
- if (!registry) {
62
- throw new Error(`Async signal "${registeredId}" is not registered.`);
63
- }
64
-
65
- if (activeAbort && !activeAbort.aborted) {
66
- activeAbort.cancel(new Error(`Async signal "${registeredId}" refreshed.`));
67
- }
68
-
69
- const runVersion = version + 1;
70
- version = runVersion;
71
- loading = true;
72
- error = null;
73
- status = "loading";
74
-
75
- const controller = new AbortController();
76
- activeController = controller;
77
- activeAbort = controller.signal;
78
- attachCancel(activeAbort, controller);
79
- notify();
80
-
81
- const context = {
82
- signals: registry,
83
- id: registeredId,
84
- get server() {
85
- const context = registry._context?.() ?? {};
86
- const server = context.server;
87
- if (typeof server?._withContext === "function") {
88
- return server._withContext({
89
- signals: registry,
90
- router: context.router,
91
- loader: context.loader,
92
- cache: context.cache,
93
- abort: activeAbort,
94
- scheduler: context.scheduler
95
- });
96
- }
97
- return server;
98
- },
99
- get router() {
100
- return registry._context?.().router;
101
- },
102
- get loader() {
103
- return registry._context?.().loader;
104
- },
105
- get cache() {
106
- return registry._context?.().cache;
107
- },
108
- get scheduler() {
109
- return registry._context?.().scheduler;
110
- },
111
- get version() {
112
- return runVersion;
113
- },
114
- get abort() {
115
- return activeAbort;
116
- },
117
- refresh() {
118
- return state.refresh();
119
- }
120
- };
121
-
122
- let outcome;
123
- try {
124
- outcome = registry._collectDependencies(() => fn.call(context));
125
- } catch (cause) {
126
- finishError(runVersion, cause);
127
- return Promise.reject(cause);
128
- }
129
-
130
- syncDependencies(outcome.dependencies);
131
-
132
- return Promise.resolve(outcome.value).then(
133
- (nextValue) => {
134
- if (!isCurrent(runVersion)) {
135
- return value;
136
- }
137
- value = nextValue;
138
- loading = false;
139
- error = null;
140
- status = "ready";
141
- notify();
142
- return value;
143
- },
144
- (cause) => {
145
- if (!isCurrent(runVersion)) {
146
- return value;
147
- }
148
- if (activeAbort?.aborted) {
149
- loading = false;
150
- status = value === undefined ? "idle" : "ready";
151
- notify();
152
- return value;
153
- }
154
- finishError(runVersion, cause);
155
- return value;
156
- }
157
- );
158
- },
159
-
160
- cancel(reason) {
161
- if (activeAbort && !activeAbort.aborted) {
162
- activeAbort.cancel(reason);
163
- }
164
- },
165
-
166
- subscribe(fn) {
167
- if (typeof fn !== "function") {
168
- throw new TypeError("subscribe(fn) requires a function.");
169
- }
170
- subscribers.add(fn);
171
- return () => subscribers.delete(fn);
172
- },
173
-
174
- snapshot() {
175
- return {
176
- value,
177
- loading,
178
- error,
179
- status,
180
- version
181
- };
182
- },
183
-
184
- _bindRegistry(nextRegistry, nextId) {
185
- registry = nextRegistry;
186
- registeredId = nextId;
187
- const start = () => {
188
- if (registry === nextRegistry && status === "idle") {
189
- state.refresh();
190
- }
191
- };
192
- const scheduler = registry._context?.().scheduler;
193
- if (scheduler) {
194
- scheduler.enqueue("async", start, {
195
- scope: registeredId,
196
- key: `asyncSignal:${registeredId}:initial`
197
- });
198
- } else {
199
- queueMicrotask(start);
200
- }
201
- },
202
-
203
- _dispose() {
204
- state.cancel(new Error(`Async signal "${registeredId}" disposed.`));
205
- for (const cleanup of dependencyCleanups) {
206
- cleanup();
207
- }
208
- dependencyCleanups.clear();
209
- subscribers.clear();
210
- }
211
- };
212
-
213
- function finishError(runVersion, cause) {
214
- if (!isCurrent(runVersion)) {
215
- return;
216
- }
217
- loading = false;
218
- error = cause;
219
- status = "error";
220
- notify();
221
- }
222
-
223
- function isCurrent(runVersion) {
224
- return runVersion === version && activeController?.signal === activeAbort;
225
- }
226
-
227
- function syncDependencies(dependencies) {
228
- for (const cleanup of dependencyCleanups) {
229
- cleanup();
230
- }
231
- dependencyCleanups.clear();
232
-
233
- for (const dependency of dependencies) {
234
- const dependencyId = String(dependency).split(".")[0];
235
- if (dependencyId && dependencyId !== registeredId) {
236
- dependencyCleanups.add(registry.subscribe(dependency, () => scheduleRefresh()));
237
- }
238
- }
239
- }
240
-
241
- function scheduleRefresh() {
242
- if (activeAbort && !activeAbort.aborted) {
243
- activeAbort.cancel(new Error(`Async signal "${registeredId}" dependency changed.`));
244
- }
245
- const scheduler = registry?._context?.().scheduler;
246
- if (!scheduler) {
247
- state.refresh();
248
- return;
249
- }
250
- scheduler.enqueue("async", () => state.refresh(), {
251
- scope: registeredId,
252
- key: `asyncSignal:${registeredId}:refresh`
253
- });
254
- }
255
-
256
- function notify() {
257
- for (const subscriber of [...subscribers]) {
258
- subscriber(state);
259
- }
260
- }
261
-
262
- return state;
263
- }
264
-
265
- export function isAsyncSignal(value) {
266
- return Boolean(value?.[asyncSignalKind]);
267
- }
268
-
269
- function attachCancel(signal, controller) {
270
- Object.defineProperty(signal, "cancel", {
271
- configurable: true,
272
- enumerable: false,
273
- value(reason) {
274
- controller.abort(reason);
275
- }
276
- });
277
- }
package/src/attributes.js DELETED
@@ -1,52 +0,0 @@
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
- }
@@ -1,302 +0,0 @@
1
- const defaultRecentLimit = 50;
2
-
3
- export function createBoundaryReceiver(options = {}) {
4
- const loader = options.loader;
5
- const signals = options.signals ?? loader?.signals;
6
- const cache = options.cache ?? loader?.cache;
7
- const scheduler = options.scheduler ?? loader?.scheduler;
8
- const router = options.router ?? loader?.router;
9
- const recentLimit = options.recentLimit ?? defaultRecentLimit;
10
- const throwOnError = options.throwOnError === true;
11
- const onApply = typeof options.onApply === "function" ? options.onApply : undefined;
12
- const onIgnore = typeof options.onIgnore === "function" ? options.onIgnore : undefined;
13
- const onError = typeof options.onError === "function" ? options.onError : undefined;
14
- const isScopeDestroyed = typeof options.isScopeDestroyed === "function"
15
- ? options.isScopeDestroyed
16
- : (scope) => scheduler?.isScopeDestroyed?.(scope) ?? scheduler?.inspectDestroyed?.(scope) ?? false;
17
-
18
- if (!loader || typeof loader.swap !== "function") {
19
- throw new TypeError("createBoundaryReceiver(...) requires a loader with swap(boundary, html).");
20
- }
21
- if (!Number.isInteger(recentLimit) || recentLimit < 0) {
22
- throw new TypeError("createBoundaryReceiver(...) recentLimit must be a non-negative integer.");
23
- }
24
-
25
- const boundaries = new Map();
26
- const recent = [];
27
- let destroyed = false;
28
-
29
- const receiver = {
30
- async apply(patch) {
31
- if (destroyed) {
32
- throw new Error("Boundary receiver has been destroyed.");
33
- }
34
-
35
- const normalized = validatePatch(patch);
36
- const record = boundaryRecord(normalized.boundary);
37
- if (normalized.seq <= record.lastSeq) {
38
- const result = {
39
- status: "ignored-stale",
40
- boundary: normalized.boundary,
41
- seq: normalized.seq,
42
- lastSeq: record.lastSeq
43
- };
44
- record.ignored += 1;
45
- record.lastStatus = result.status;
46
- remember(result);
47
- onIgnore?.(result, patch);
48
- return result;
49
- }
50
-
51
- if (normalized.parentScope !== undefined && isScopeDestroyed(normalized.parentScope)) {
52
- const result = {
53
- status: "ignored-destroyed",
54
- boundary: normalized.boundary,
55
- seq: normalized.seq,
56
- parentScope: normalized.parentScope
57
- };
58
- record.ignored += 1;
59
- record.lastStatus = result.status;
60
- remember(result);
61
- onIgnore?.(result, patch);
62
- return result;
63
- }
64
-
65
- record.lastSeq = normalized.seq;
66
-
67
- if (Object.hasOwn(normalized, "error")) {
68
- const error = toStableError(normalized.error);
69
- const result = {
70
- status: "errored",
71
- boundary: normalized.boundary,
72
- seq: normalized.seq,
73
- error
74
- };
75
- record.errored += 1;
76
- record.lastStatus = result.status;
77
- remember(result);
78
- onError?.(error, result, patch);
79
- if (throwOnError) {
80
- throw error;
81
- }
82
- return result;
83
- }
84
-
85
- if (normalized.signals) {
86
- if (!signals || typeof signals.set !== "function") {
87
- throw new Error("Boundary patch includes signals, but no signal registry is available.");
88
- }
89
- for (const [path, value] of Object.entries(normalized.signals)) {
90
- signals.set(path, value);
91
- }
92
- }
93
-
94
- if (normalized.cache?.browser) {
95
- if (!cache || typeof cache.restore !== "function") {
96
- throw new Error("Boundary patch includes browser cache, but no cache registry is available.");
97
- }
98
- cache.restore(normalized.cache.browser);
99
- }
100
-
101
- if (normalized.html != null) {
102
- loader.swap(normalized.boundary, normalized.html);
103
- }
104
-
105
- await flushScheduler(scheduler, normalized.scope);
106
-
107
- if (normalized.redirect) {
108
- await followRedirect(normalized.redirect, router, loader);
109
- const result = {
110
- status: "redirected",
111
- boundary: normalized.boundary,
112
- seq: normalized.seq,
113
- redirect: normalized.redirect
114
- };
115
- record.applied += 1;
116
- record.lastStatus = result.status;
117
- remember(result);
118
- onApply?.(result, patch);
119
- return result;
120
- }
121
-
122
- const result = {
123
- status: "applied",
124
- boundary: normalized.boundary,
125
- seq: normalized.seq
126
- };
127
- record.applied += 1;
128
- record.lastStatus = result.status;
129
- remember(result);
130
- onApply?.(result, patch);
131
- return result;
132
- },
133
-
134
- inspect() {
135
- const snapshot = {};
136
- for (const [boundary, record] of boundaries) {
137
- snapshot[boundary] = {
138
- lastSeq: record.lastSeq,
139
- applied: record.applied,
140
- ignored: record.ignored,
141
- lastStatus: record.lastStatus
142
- };
143
- if (record.errored > 0) {
144
- snapshot[boundary].errored = record.errored;
145
- }
146
- }
147
- return {
148
- destroyed,
149
- boundaries: snapshot,
150
- recent: recent.map((entry) => ({ ...entry }))
151
- };
152
- },
153
-
154
- reset(boundary) {
155
- if (boundary === undefined) {
156
- boundaries.clear();
157
- recent.length = 0;
158
- return receiver;
159
- }
160
- assertBoundary(boundary);
161
- boundaries.delete(boundary);
162
- for (let index = recent.length - 1; index >= 0; index -= 1) {
163
- if (recent[index].boundary === boundary) {
164
- recent.splice(index, 1);
165
- }
166
- }
167
- return receiver;
168
- },
169
-
170
- destroy() {
171
- destroyed = true;
172
- boundaries.clear();
173
- recent.length = 0;
174
- }
175
- };
176
-
177
- return receiver;
178
-
179
- function boundaryRecord(boundary) {
180
- if (!boundaries.has(boundary)) {
181
- boundaries.set(boundary, {
182
- lastSeq: -Infinity,
183
- applied: 0,
184
- ignored: 0,
185
- errored: 0,
186
- lastStatus: undefined
187
- });
188
- }
189
- return boundaries.get(boundary);
190
- }
191
-
192
- function remember(result) {
193
- if (recentLimit === 0) {
194
- return;
195
- }
196
- recent.push(toRecentEntry(result));
197
- while (recent.length > recentLimit) {
198
- recent.shift();
199
- }
200
- }
201
- }
202
-
203
- function validatePatch(patch) {
204
- if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
205
- throw new TypeError("receiver.apply(patch) requires a boundary patch object.");
206
- }
207
-
208
- assertBoundary(patch.boundary);
209
- if (typeof patch.seq !== "number" || !Number.isFinite(patch.seq)) {
210
- throw new TypeError("Boundary patch seq must be a finite number.");
211
- }
212
-
213
- if (patch.signals !== undefined && !isPlainObject(patch.signals)) {
214
- throw new TypeError("Boundary patch signals must be an object.");
215
- }
216
- if (patch.cache !== undefined && !isPlainObject(patch.cache)) {
217
- throw new TypeError("Boundary patch cache must be an object.");
218
- }
219
- if (patch.cache?.browser !== undefined && !isPlainObject(patch.cache.browser)) {
220
- throw new TypeError("Boundary patch cache.browser must be an object.");
221
- }
222
- if (patch.redirect !== undefined && (typeof patch.redirect !== "string" || patch.redirect.length === 0)) {
223
- throw new TypeError("Boundary patch redirect must be a non-empty string.");
224
- }
225
- if (patch.parentScope !== undefined && typeof patch.parentScope !== "string") {
226
- throw new TypeError("Boundary patch parentScope must be a string.");
227
- }
228
- if (patch.scope !== undefined && typeof patch.scope !== "string") {
229
- throw new TypeError("Boundary patch scope must be a string.");
230
- }
231
-
232
- const hasHtml = Object.hasOwn(patch, "html") && patch.html != null;
233
- const hasSignals = patch.signals && Object.keys(patch.signals).length > 0;
234
- const hasBrowserCache = patch.cache?.browser && Object.keys(patch.cache.browser).length > 0;
235
- const hasRedirect = Boolean(patch.redirect);
236
- const hasError = Object.hasOwn(patch, "error");
237
- if (!hasHtml && !hasSignals && !hasBrowserCache && !hasRedirect && !hasError) {
238
- throw new TypeError("Boundary patch must include html, signals, cache.browser, redirect, or error.");
239
- }
240
-
241
- return patch;
242
- }
243
-
244
- function assertBoundary(boundary) {
245
- if (typeof boundary !== "string" || boundary.length === 0) {
246
- throw new TypeError("Boundary patch boundary must be a non-empty string.");
247
- }
248
- }
249
-
250
- async function flushScheduler(scheduler, scope) {
251
- if (!scheduler) {
252
- return;
253
- }
254
- if (scope !== undefined && typeof scheduler.flushScope === "function") {
255
- await scheduler.flushScope(scope);
256
- return;
257
- }
258
- if (typeof scheduler.flush === "function") {
259
- await scheduler.flush();
260
- }
261
- }
262
-
263
- async function followRedirect(redirect, router, loader) {
264
- if (router && typeof router.navigate === "function") {
265
- await router.navigate(redirect);
266
- return;
267
- }
268
- const location = loader?.root?.ownerDocument?.defaultView?.location ?? globalThis.location;
269
- location?.assign?.(redirect);
270
- }
271
-
272
- function toStableError(value) {
273
- if (value instanceof Error) {
274
- return value;
275
- }
276
- if (value && typeof value === "object" && typeof value.message === "string") {
277
- return Object.assign(new Error(value.message), value);
278
- }
279
- return new Error(String(value));
280
- }
281
-
282
- function toRecentEntry(result) {
283
- const entry = {
284
- boundary: result.boundary,
285
- seq: result.seq,
286
- status: result.status
287
- };
288
- if (result.status === "ignored-stale") {
289
- entry.lastSeq = result.lastSeq;
290
- }
291
- if (result.status === "ignored-destroyed" && result.parentScope !== undefined) {
292
- entry.parentScope = result.parentScope;
293
- }
294
- if (result.status === "redirected") {
295
- entry.redirect = result.redirect;
296
- }
297
- return entry;
298
- }
299
-
300
- function isPlainObject(value) {
301
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
302
- }
package/src/browser.js DELETED
@@ -1,18 +0,0 @@
1
- export { asyncSignal } from "./async-signal.js";
2
- export { Async, createApp, defineApp, readSnapshot } from "./app.js";
3
- export { attributeName, defineAttributeConfig } from "./attributes.js";
4
- export { createBoundaryReceiver } from "./boundary-receiver.js";
5
- export { createCacheRegistry, defineCache } from "./cache.js";
6
- export { component, createComponentRegistry, defineComponent } from "./component.js";
7
- export { delay } from "./delay.js";
8
- export { defineAsyncContainerElement, defineAsyncSuspenseElement } from "./elements.js";
9
- export { createHandlerRegistry } from "./handlers.js";
10
- export { html } from "./html.js";
11
- export { createLazyRegistry, defineRegistrySnapshot } from "./lazy-registry.js";
12
- export { Loader, AsyncLoader } from "./loader.js";
13
- export { createPartialRegistry } from "./partials.js";
14
- export { createRegistryStore } from "./registry-store.js";
15
- export { createRouteRegistry, createRouter, defineRoute, route } from "./router.js";
16
- export { createScheduler } from "./scheduler.js";
17
- export { applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult } from "./server.js";
18
- export { computed, createSignal, createSignalRegistry, effect, signal } from "./signals.js";