@async/framework 0.6.0 → 0.8.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
@@ -4,16 +4,18 @@ import { createHandlerRegistry } from "./handlers.js";
4
4
  import { Loader } from "./loader.js";
5
5
  import { createPartialRegistry } from "./partials.js";
6
6
  import { createRouteRegistry, createRouter } from "./router.js";
7
- import { createServerRegistry } from "./server.js";
7
+ import { createScheduler } from "./scheduler.js";
8
+ import { createServerNamespace } from "./server.js";
8
9
  import { createSignal, createSignalRegistry } from "./signals.js";
9
10
  import { createRegistryStore } from "./registry-store.js";
10
11
  import { attributeName, normalizeAttributeConfig } from "./attributes.js";
11
12
 
12
13
  const registryTypes = new Set(["signal", "handler", "server", "partial", "route", "component"]);
13
14
 
14
- export function defineApp(initial) {
15
+ export function defineApp(initial, options = {}) {
15
16
  const registry = createRegistryStore(undefined, { target: "browser" });
16
17
  const runtimes = new Set();
18
+ const createRuntime = options.createRuntime ?? createApp;
17
19
 
18
20
  const app = {
19
21
  registry,
@@ -32,7 +34,7 @@ export function defineApp(initial) {
32
34
  },
33
35
 
34
36
  start(options = {}) {
35
- const runtime = createApp(app, options).start();
37
+ const runtime = createRuntime(app, options).start();
36
38
  app.runtime = runtime;
37
39
  return runtime;
38
40
  },
@@ -57,13 +59,18 @@ export function defineApp(initial) {
57
59
  export function createApp(appOrDefinition = Async, options = {}) {
58
60
  const app = isAppHub(appOrDefinition) ? appOrDefinition : defineApp(appOrDefinition ?? {});
59
61
  const target = options.target ?? "browser";
62
+ const scheduler = options.scheduler ?? options.loader?.scheduler ?? createScheduler({
63
+ strategy: target === "server" ? "manual" : "microtask"
64
+ });
65
+ const ownsScheduler = !options.scheduler && !options.loader?.scheduler;
60
66
  const attributes = normalizeAttributeConfig(options.attributes);
61
67
  const registry = options.registry ?? app.registry.view({ target });
62
68
  const signals = options.signals ?? createSignalRegistry(undefined, { registry, type: "signal" });
63
69
  const handlers = options.handlers ?? createHandlerRegistry(undefined, { registry, type: "handler" });
64
70
  const serverCache = createCacheRegistry(undefined, { registry, type: "cache.server" });
65
71
  const browserCache = createCacheRegistry(undefined, { registry, type: "cache.browser" });
66
- const server = options.server ?? createServerRegistry(undefined, { registry, type: "server" });
72
+ const serverFactory = options.serverFactory ?? createServerReferenceRegistry;
73
+ const server = options.server ?? serverFactory(undefined, { registry, type: "server" });
67
74
  const partials = options.partials ?? createPartialRegistry(undefined, { registry, type: "partial" });
68
75
  const routes = options.routes ?? createRouteRegistry(undefined, { registry, type: "route" });
69
76
  const components = options.components ?? createComponentRegistry(undefined, { registry, type: "component" });
@@ -73,7 +80,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
73
80
  let started = false;
74
81
  let destroyed = false;
75
82
 
76
- applySnapshot(signals, browserCache, options.snapshot);
83
+ applySnapshot(signals, browserCache, options.snapshot ?? (target === "browser" ? readSnapshot(options.root, { attributes }) : undefined));
77
84
  attachServerCache(server, serverCache);
78
85
 
79
86
  const runtime = {
@@ -91,6 +98,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
91
98
  },
92
99
  loader,
93
100
  router,
101
+ scheduler,
94
102
  attributes,
95
103
 
96
104
  start() {
@@ -107,12 +115,13 @@ export function createApp(appOrDefinition = Async, options = {}) {
107
115
  handlers,
108
116
  server,
109
117
  cache: browserCache,
118
+ scheduler,
110
119
  attributes
111
120
  });
112
121
  runtime.loader = loader;
113
122
 
114
123
  configureServerContext({ cache: browserCache });
115
- signals._setContext?.({ server, loader, cache: browserCache });
124
+ signals._setContext?.({ server, loader, cache: browserCache, scheduler });
116
125
 
117
126
  loader.start();
118
127
 
@@ -128,6 +137,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
128
137
  server,
129
138
  cache: browserCache,
130
139
  partials,
140
+ scheduler,
131
141
  fetch: options.fetch,
132
142
  routeEndpoint: options.routeEndpoint,
133
143
  attributes
@@ -139,7 +149,7 @@ export function createApp(appOrDefinition = Async, options = {}) {
139
149
  }
140
150
  } else {
141
151
  configureServerContext({ cache: serverCache });
142
- signals._setContext?.({ server, cache: serverCache });
152
+ signals._setContext?.({ server, cache: serverCache, scheduler });
143
153
  }
144
154
 
145
155
  return runtime;
@@ -153,9 +163,10 @@ export function createApp(appOrDefinition = Async, options = {}) {
153
163
  async render(url) {
154
164
  assertActive();
155
165
  configureServerContext({ cache: serverCache });
156
- signals._setContext?.({ server, cache: serverCache });
166
+ signals._setContext?.({ server, cache: serverCache, scheduler });
157
167
  const matched = routes.match(url);
158
168
  if (!matched) {
169
+ await scheduler.flush();
159
170
  return {
160
171
  html: renderDocument("", { status: 404, signals, browserCache, boundary: options.boundary ?? "route", attributes }),
161
172
  status: 404,
@@ -175,8 +186,8 @@ export function createApp(appOrDefinition = Async, options = {}) {
175
186
  cache: serverCache,
176
187
  browserCache,
177
188
  partials,
178
- request: options.request,
179
- locals: options.locals
189
+ scheduler,
190
+ ...currentRequestContext()
180
191
  })
181
192
  : { html: "" };
182
193
 
@@ -189,6 +200,8 @@ export function createApp(appOrDefinition = Async, options = {}) {
189
200
  browserCache.restore(result.cache.browser);
190
201
  }
191
202
 
203
+ await scheduler.flush();
204
+
192
205
  const status = result.status ?? 200;
193
206
  return {
194
207
  html: renderDocument(result.html, { status, signals, browserCache, boundary: result.boundary ?? options.boundary ?? "route", attributes }),
@@ -207,6 +220,9 @@ export function createApp(appOrDefinition = Async, options = {}) {
207
220
  router?.destroy?.();
208
221
  loader?.destroy?.();
209
222
  signals.destroy?.();
223
+ if (ownsScheduler) {
224
+ scheduler.destroy();
225
+ }
210
226
  },
211
227
 
212
228
  _applyUse(normalized) {
@@ -227,11 +243,23 @@ export function createApp(appOrDefinition = Async, options = {}) {
227
243
  loader,
228
244
  router,
229
245
  cache,
230
- request: options.request,
231
- locals: options.locals
246
+ scheduler,
247
+ requestContext: options.requestContext,
248
+ ...currentRequestContext()
232
249
  });
233
250
  }
234
251
 
252
+ function currentRequestContext() {
253
+ const context = readRequestContextLike(options.requestContext);
254
+ return {
255
+ requestContext: context,
256
+ request: context.request ?? options.request,
257
+ headers: context.headers ?? options.headers,
258
+ cookies: context.cookies ?? options.cookies,
259
+ locals: context.locals ?? options.locals
260
+ };
261
+ }
262
+
235
263
  function assertActive() {
236
264
  if (destroyed) {
237
265
  throw new Error("Async app runtime has been destroyed.");
@@ -241,6 +269,38 @@ export function createApp(appOrDefinition = Async, options = {}) {
241
269
 
242
270
  export const Async = defineApp();
243
271
 
272
+ export function readSnapshot(root = globalThis.document, { attributes } = {}) {
273
+ const attributeConfig = normalizeAttributeConfig(attributes);
274
+ const snapshotAttr = attributeName(attributeConfig, "async", "snapshot");
275
+ const documentRef = root?.ownerDocument ?? root ?? globalThis.document;
276
+ const rootNode = root ?? documentRef;
277
+ if (!rootNode?.querySelectorAll && !documentRef?.querySelectorAll) {
278
+ return {};
279
+ }
280
+
281
+ for (const searchRoot of new Set([rootNode, documentRef])) {
282
+ if (!searchRoot?.querySelectorAll) {
283
+ continue;
284
+ }
285
+ for (const script of searchRoot.querySelectorAll("script[type='application/json'], script")) {
286
+ if (!script.hasAttribute?.(snapshotAttr)) {
287
+ continue;
288
+ }
289
+ const source = script.textContent?.trim() ?? "";
290
+ if (!source) {
291
+ return {};
292
+ }
293
+ try {
294
+ return JSON.parse(source);
295
+ } catch (cause) {
296
+ throw new Error(`Could not parse Async snapshot: ${cause instanceof Error ? cause.message : String(cause)}`);
297
+ }
298
+ }
299
+ }
300
+
301
+ return {};
302
+ }
303
+
244
304
  function applyUseToRuntime(runtime, normalized) {
245
305
  applyRegistryUse(runtime.signals, runtime.registry, normalized.signal);
246
306
  applyRegistryUse(runtime.handlers, runtime.registry, normalized.handler);
@@ -353,6 +413,77 @@ function attachServerCache(server, cache) {
353
413
  }
354
414
  }
355
415
 
416
+ function createServerReferenceRegistry(initialMap = {}, options = {}) {
417
+ const registry = options.registry ?? createRegistryStore();
418
+ const type = options.type ?? "server";
419
+ const defaults = {};
420
+
421
+ const reference = {
422
+ registry,
423
+
424
+ register(id, value) {
425
+ registry.register(type, id, value);
426
+ return id;
427
+ },
428
+
429
+ registerMany(map) {
430
+ for (const [id, value] of Object.entries(map ?? {})) {
431
+ reference.register(id, value);
432
+ }
433
+ return reference;
434
+ },
435
+
436
+ unregister(id) {
437
+ return registry.unregister(type, id);
438
+ },
439
+
440
+ resolve() {
441
+ return undefined;
442
+ },
443
+
444
+ async run(id) {
445
+ throw new Error(`Server command "${id}" cannot run without a server proxy or server registry.`);
446
+ },
447
+
448
+ keys() {
449
+ return registry.keys(type);
450
+ },
451
+
452
+ entries() {
453
+ return registry.entries(type);
454
+ },
455
+
456
+ inspect() {
457
+ return registry.entries(type);
458
+ },
459
+
460
+ _setContext(context = {}) {
461
+ Object.assign(defaults, context);
462
+ return reference;
463
+ },
464
+
465
+ _adoptMany() {
466
+ return reference;
467
+ }
468
+ };
469
+
470
+ reference.registerMany(initialMap);
471
+ return createServerNamespace((id, args, context) => reference.run(id, args, context), reference, () => defaults);
472
+ }
473
+
474
+ function readRequestContextLike(store) {
475
+ if (!store) {
476
+ return {};
477
+ }
478
+ if (typeof store.get === "function") {
479
+ return store.get() ?? {};
480
+ }
481
+ if (typeof store.getStore === "function") {
482
+ return store.getStore() ?? {};
483
+ }
484
+ return {};
485
+ }
486
+
356
487
  function normalizeEntries(type, entries = {}) {
357
488
  if (type !== "signal") {
358
489
  return { ...(entries ?? {}) };
@@ -90,7 +90,8 @@ export function asyncSignal(id, fn) {
90
90
  router: context.router,
91
91
  loader: context.loader,
92
92
  cache: context.cache,
93
- abort: activeAbort
93
+ abort: activeAbort,
94
+ scheduler: context.scheduler
94
95
  });
95
96
  }
96
97
  return server;
@@ -104,6 +105,9 @@ export function asyncSignal(id, fn) {
104
105
  get cache() {
105
106
  return registry._context?.().cache;
106
107
  },
108
+ get scheduler() {
109
+ return registry._context?.().scheduler;
110
+ },
107
111
  get version() {
108
112
  return runVersion;
109
113
  },
@@ -180,11 +184,20 @@ export function asyncSignal(id, fn) {
180
184
  _bindRegistry(nextRegistry, nextId) {
181
185
  registry = nextRegistry;
182
186
  registeredId = nextId;
183
- queueMicrotask(() => {
187
+ const start = () => {
184
188
  if (registry === nextRegistry && status === "idle") {
185
189
  state.refresh();
186
190
  }
187
- });
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
+ }
188
201
  },
189
202
 
190
203
  _dispose() {
@@ -220,11 +233,26 @@ export function asyncSignal(id, fn) {
220
233
  for (const dependency of dependencies) {
221
234
  const dependencyId = String(dependency).split(".")[0];
222
235
  if (dependencyId && dependencyId !== registeredId) {
223
- dependencyCleanups.add(registry.subscribe(dependency, () => state.refresh()));
236
+ dependencyCleanups.add(registry.subscribe(dependency, () => scheduleRefresh()));
224
237
  }
225
238
  }
226
239
  }
227
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
+
228
256
  function notify() {
229
257
  for (const subscriber of [...subscribers]) {
230
258
  subscriber(state);
package/src/browser.js ADDED
@@ -0,0 +1,15 @@
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 { createCacheRegistry, defineCache } from "./cache.js";
5
+ export { component, createComponentRegistry, defineComponent } from "./component.js";
6
+ export { delay } from "./delay.js";
7
+ export { createHandlerRegistry } from "./handlers.js";
8
+ export { html } from "./html.js";
9
+ export { Loader, AsyncLoader } from "./loader.js";
10
+ export { createPartialRegistry } from "./partials.js";
11
+ export { createRegistryStore } from "./registry-store.js";
12
+ export { createRouteRegistry, createRouter, defineRoute, route } from "./router.js";
13
+ export { createScheduler } from "./scheduler.js";
14
+ export { applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult } from "./server.js";
15
+ export { computed, createSignal, createSignalRegistry, effect, signal } from "./signals.js";
package/src/cache.js CHANGED
@@ -15,6 +15,7 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
15
15
  const registryStore = registry ?? createRegistryStore();
16
16
  const definitions = registryStore._map(type);
17
17
  const entries = registryStore._map(`${type}.entries`);
18
+ const pending = new Map();
18
19
 
19
20
  const registryApi = attachRegistryInspection({
20
21
  register(id, definition = defineCache()) {
@@ -76,19 +77,37 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
76
77
  if (cached !== undefined) {
77
78
  return cached;
78
79
  }
79
- const value = await fn();
80
- registryApi.set(key, value, options);
81
- return value;
80
+ if (pending.has(key)) {
81
+ return pending.get(key);
82
+ }
83
+ let promise;
84
+ promise = Promise.resolve()
85
+ .then(fn)
86
+ .then((value) => {
87
+ if (pending.get(key) === promise) {
88
+ registryApi.set(key, value, options);
89
+ }
90
+ return value;
91
+ })
92
+ .finally(() => {
93
+ if (pending.get(key) === promise) {
94
+ pending.delete(key);
95
+ }
96
+ });
97
+ pending.set(key, promise);
98
+ return promise;
82
99
  },
83
100
 
84
101
  delete(key) {
85
102
  assertKey(key);
103
+ pending.delete(key);
86
104
  return entries.delete(key);
87
105
  },
88
106
 
89
107
  clear(prefix) {
90
108
  if (prefix === undefined) {
91
109
  entries.clear();
110
+ pending.clear();
92
111
  return registryApi;
93
112
  }
94
113
  for (const key of [...entries.keys()]) {
@@ -96,6 +115,11 @@ export function createCacheRegistry(initialMap = {}, { now = () => Date.now(), r
96
115
  entries.delete(key);
97
116
  }
98
117
  }
118
+ for (const key of [...pending.keys()]) {
119
+ if (key.startsWith(prefix)) {
120
+ pending.delete(key);
121
+ }
122
+ }
99
123
  return registryApi;
100
124
  },
101
125
 
package/src/component.js CHANGED
@@ -113,10 +113,15 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
113
113
  html,
114
114
  attach(target) {
115
115
  for (const hook of attachHooks) {
116
- const cleanup = hook(target);
117
- if (typeof cleanup === "function") {
118
- cleanups.push(cleanup);
119
- }
116
+ runtime.scheduler?.enqueue("lifecycle", () => {
117
+ const cleanup = hook(target);
118
+ if (typeof cleanup === "function") {
119
+ cleanups.push(cleanup);
120
+ }
121
+ }, {
122
+ scope,
123
+ key: `attach:${attachHooks.indexOf(hook)}`
124
+ }) ?? runAttachHook(hook, target);
120
125
  }
121
126
  },
122
127
  mount(target) {
@@ -124,7 +129,17 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
124
129
  },
125
130
  visible(target, observeVisible) {
126
131
  for (const hook of visibleHooks) {
127
- const cleanup = observeVisible(target, hook);
132
+ const cleanup = observeVisible(target, () => {
133
+ runtime.scheduler?.enqueue("lifecycle", () => {
134
+ const hookCleanup = hook(target);
135
+ if (typeof hookCleanup === "function") {
136
+ cleanups.push(hookCleanup);
137
+ }
138
+ }, {
139
+ scope,
140
+ key: `visible:${visibleHooks.indexOf(hook)}`
141
+ }) ?? runVisibleHook(hook, target);
142
+ });
128
143
  if (typeof cleanup === "function") {
129
144
  cleanups.push(cleanup);
130
145
  }
@@ -134,6 +149,7 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
134
149
  while (destroyHooks.length > 0) {
135
150
  destroyHooks.pop()?.();
136
151
  }
152
+ runtime.scheduler?.markScopeDestroyed(scope);
137
153
  while (cleanups.length > 0) {
138
154
  cleanups.pop()?.();
139
155
  }
@@ -142,10 +158,24 @@ export function renderComponent(Component, props = {}, runtime, parentScope = "c
142
158
  }
143
159
  }
144
160
  };
161
+
162
+ function runAttachHook(hook, target) {
163
+ const cleanup = hook(target);
164
+ if (typeof cleanup === "function") {
165
+ cleanups.push(cleanup);
166
+ }
167
+ }
168
+
169
+ function runVisibleHook(hook, target) {
170
+ const cleanup = hook(target);
171
+ if (typeof cleanup === "function") {
172
+ cleanups.push(cleanup);
173
+ }
174
+ }
145
175
  }
146
176
 
147
177
  function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
148
- const { signals, handlers, loader, server, router, cache } = runtime;
178
+ const { signals, handlers, loader, server, router, cache, scheduler } = runtime;
149
179
  const generatedHandlers = new WeakMap();
150
180
  let generatedHandlerCounter = 0;
151
181
  let generatedSignalCounter = 0;
@@ -157,6 +187,7 @@ function createComponentContext({ runtime, scope, cleanups, attachHooks, visible
157
187
  server,
158
188
  router,
159
189
  cache,
190
+ scheduler,
160
191
 
161
192
  signal(name, initial) {
162
193
  if (arguments.length === 1) {
@@ -201,7 +232,11 @@ function createComponentContext({ runtime, scope, cleanups, attachHooks, visible
201
232
  },
202
233
 
203
234
  effect(fn) {
204
- const cleanup = signals.effect(() => fn.call(context));
235
+ const cleanup = signals.effect(() => fn.call(context), {
236
+ scheduler,
237
+ phase: "effect",
238
+ scope
239
+ });
205
240
  cleanups.push(cleanup);
206
241
  return cleanup;
207
242
  },
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { asyncSignal } from "./async-signal.js";
2
- export { Async, createApp, defineApp } from "./app.js";
2
+ export { Async, createApp, defineApp, readSnapshot } from "./server-entry.js";
3
3
  export { attributeName, defineAttributeConfig } from "./attributes.js";
4
4
  export { createCacheRegistry, defineCache } from "./cache.js";
5
5
  export { component, createComponentRegistry, defineComponent } from "./component.js";
@@ -10,5 +10,8 @@ export { Loader, AsyncLoader } from "./loader.js";
10
10
  export { createPartialRegistry } from "./partials.js";
11
11
  export { createRegistryStore } from "./registry-store.js";
12
12
  export { createRouteRegistry, createRouter, defineRoute, route } from "./router.js";
13
- export { createServerProxy, createServerRegistry } from "./server.js";
13
+ export { createScheduler } from "./scheduler.js";
14
+ export { createRequestContextStore } from "./request-context.js";
15
+ export { applyServerResult, createServerProxy, resolveServerCommandArguments, unwrapServerResult } from "./server.js";
16
+ export { createServerRegistry } from "./server-registry.js";
14
17
  export { computed, createSignal, createSignalRegistry, effect, signal } from "./signals.js";