@async/framework 0.7.0 → 0.9.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.
@@ -0,0 +1,20 @@
1
+ import {
2
+ createApp as createBaseApp,
3
+ defineApp as defineBaseApp,
4
+ readSnapshot
5
+ } from "./app.js";
6
+ import { createServerRegistry } from "./server-registry.js";
7
+
8
+ export function createApp(appOrDefinition = Async, options = {}) {
9
+ return createBaseApp(appOrDefinition, {
10
+ serverFactory: createServerRegistry,
11
+ ...options
12
+ });
13
+ }
14
+
15
+ export function defineApp(initial) {
16
+ return defineBaseApp(initial, { createRuntime: createApp });
17
+ }
18
+
19
+ export const Async = defineApp();
20
+ export { readSnapshot };
@@ -0,0 +1,97 @@
1
+ import { readRequestContext } from "./request-context.js";
2
+ import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
3
+ import { assertServerId, createServerNamespace, createSignalReader } from "./server.js";
4
+
5
+ export function createServerRegistry(initialMap = {}, options = {}) {
6
+ const registryStore = options.registry ?? createRegistryStore();
7
+ const type = options.type ?? "server";
8
+ const entries = registryStore._map(type);
9
+ const defaults = {};
10
+
11
+ const registry = attachRegistryInspection({
12
+ register(id, fn) {
13
+ assertServerId(id);
14
+ if (typeof fn !== "function") {
15
+ throw new TypeError(`Server function "${id}" must be a function.`);
16
+ }
17
+ if (entries.has(id)) {
18
+ throw new Error(`Server function "${id}" is already registered.`);
19
+ }
20
+ entries.set(id, fn);
21
+ return id;
22
+ },
23
+
24
+ registerMany(map) {
25
+ for (const [id, fn] of Object.entries(map ?? {})) {
26
+ registry.register(id, fn);
27
+ }
28
+ return registry;
29
+ },
30
+
31
+ unregister(id) {
32
+ assertServerId(id);
33
+ return entries.delete(id);
34
+ },
35
+
36
+ resolve(id) {
37
+ assertServerId(id);
38
+ return entries.get(id);
39
+ },
40
+
41
+ async run(id, args = [], context = {}) {
42
+ assertServerId(id);
43
+ const fn = registry.resolve(id);
44
+ if (!fn) {
45
+ throw new Error(`Server function "${id}" is not registered.`);
46
+ }
47
+
48
+ let runContext;
49
+ const server = createServerNamespace((childId, childArgs, childContext = {}) => {
50
+ return registry.run(childId, childArgs, { ...runContext, ...childContext });
51
+ }, {}, () => runContext);
52
+
53
+ const mergedContext = mergeRequestContext({
54
+ ...defaults,
55
+ ...context,
56
+ cache: defaults.cache ?? context.cache
57
+ });
58
+
59
+ runContext = {
60
+ ...mergedContext,
61
+ id,
62
+ args,
63
+ input: mergedContext.input,
64
+ signals: createSignalReader(mergedContext.signals),
65
+ abort: mergedContext.abort,
66
+ cache: mergedContext.cache,
67
+ server
68
+ };
69
+
70
+ return fn.call(runContext, ...args);
71
+ },
72
+
73
+ _setContext(context = {}) {
74
+ Object.assign(defaults, context);
75
+ return registry;
76
+ },
77
+
78
+ _adoptMany() {
79
+ return registry;
80
+ }
81
+ }, registryStore, type);
82
+
83
+ registry.registerMany(initialMap);
84
+ return createServerNamespace((id, args, context) => registry.run(id, args, context), registry, () => defaults);
85
+ }
86
+
87
+ function mergeRequestContext(context) {
88
+ const requestContext = readRequestContext(context.requestContext);
89
+ return {
90
+ ...context,
91
+ requestContext,
92
+ request: requestContext.request ?? context.request,
93
+ headers: requestContext.headers ?? context.headers,
94
+ cookies: requestContext.cookies ?? context.cookies,
95
+ locals: requestContext.locals ?? context.locals
96
+ };
97
+ }
package/src/server.js CHANGED
@@ -1,91 +1,7 @@
1
- import { attachRegistryInspection, createRegistryStore } from "./registry-store.js";
2
-
3
1
  const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
4
2
  const appliedServerResult = Symbol.for("@async/framework.appliedServerResult");
5
3
  const appliedServerValues = new WeakSet();
6
4
 
7
- export function createServerRegistry(initialMap = {}, options = {}) {
8
- const registryStore = options.registry ?? createRegistryStore();
9
- const type = options.type ?? "server";
10
- const entries = registryStore._map(type);
11
- const defaults = {};
12
-
13
- const registry = attachRegistryInspection({
14
- register(id, fn) {
15
- assertServerId(id);
16
- if (typeof fn !== "function") {
17
- throw new TypeError(`Server function "${id}" must be a function.`);
18
- }
19
- if (entries.has(id)) {
20
- throw new Error(`Server function "${id}" is already registered.`);
21
- }
22
- entries.set(id, fn);
23
- return id;
24
- },
25
-
26
- registerMany(map) {
27
- for (const [id, fn] of Object.entries(map ?? {})) {
28
- registry.register(id, fn);
29
- }
30
- return registry;
31
- },
32
-
33
- unregister(id) {
34
- assertServerId(id);
35
- return entries.delete(id);
36
- },
37
-
38
- resolve(id) {
39
- assertServerId(id);
40
- return entries.get(id);
41
- },
42
-
43
- async run(id, args = [], context = {}) {
44
- assertServerId(id);
45
- const fn = registry.resolve(id);
46
- if (!fn) {
47
- throw new Error(`Server function "${id}" is not registered.`);
48
- }
49
-
50
- let runContext;
51
- const server = createServerNamespace((childId, childArgs, childContext = {}) => {
52
- return registry.run(childId, childArgs, { ...runContext, ...childContext });
53
- }, {}, () => runContext);
54
-
55
- const mergedContext = {
56
- ...defaults,
57
- ...context,
58
- cache: defaults.cache ?? context.cache
59
- };
60
-
61
- runContext = {
62
- ...mergedContext,
63
- id,
64
- args,
65
- input: mergedContext.input,
66
- signals: createSignalReader(mergedContext.signals),
67
- abort: mergedContext.abort,
68
- cache: mergedContext.cache,
69
- server
70
- };
71
-
72
- return fn.call(runContext, ...args);
73
- },
74
-
75
- _setContext(context = {}) {
76
- Object.assign(defaults, context);
77
- return registry;
78
- },
79
-
80
- _adoptMany() {
81
- return registry;
82
- }
83
- }, registryStore, type);
84
-
85
- registry.registerMany(initialMap);
86
- return createServerNamespace((id, args, context) => registry.run(id, args, context), registry, () => defaults);
87
- }
88
-
89
5
  export function createServerProxy({
90
6
  endpoint = "/__async/server",
91
7
  fetch: fetchImpl = globalThis.fetch?.bind(globalThis),
@@ -93,13 +9,14 @@ export function createServerProxy({
93
9
  loader,
94
10
  router,
95
11
  cache,
12
+ scheduler,
96
13
  headers = {}
97
14
  } = {}) {
98
15
  if (typeof fetchImpl !== "function") {
99
16
  throw new TypeError("createServerProxy(...) requires fetch to be available.");
100
17
  }
101
18
 
102
- const defaults = { signals, loader, router, cache };
19
+ const defaults = { signals, loader, router, cache, scheduler };
103
20
 
104
21
  async function run(id, args = [], context = {}) {
105
22
  assertServerId(id);
@@ -229,7 +146,7 @@ export function defaultInput(context = {}) {
229
146
  };
230
147
  }
231
148
 
232
- function createServerNamespace(run, root = {}, contextProvider = () => ({})) {
149
+ export function createServerNamespace(run, root = {}, contextProvider = () => ({})) {
233
150
  const cache = new Map();
234
151
 
235
152
  function namespace(parts) {
@@ -311,7 +228,7 @@ function readSignal(signals, path) {
311
228
  return signals.get(path);
312
229
  }
313
230
 
314
- function createSignalReader(signals) {
231
+ export function createSignalReader(signals) {
315
232
  if (!signals || typeof signals.get === "function") {
316
233
  return signals;
317
234
  }
@@ -433,7 +350,7 @@ function toError(value) {
433
350
  return new Error(String(value));
434
351
  }
435
352
 
436
- function assertServerId(id) {
353
+ export function assertServerId(id) {
437
354
  if (typeof id !== "string" || id.length === 0) {
438
355
  throw new TypeError("Server function id must be a non-empty string.");
439
356
  }
package/src/signals.js CHANGED
@@ -96,7 +96,8 @@ export function computed(fn) {
96
96
  server: registry._context?.().server,
97
97
  router: registry._context?.().router,
98
98
  loader: registry._context?.().loader,
99
- cache: registry._context?.().cache
99
+ cache: registry._context?.().cache,
100
+ scheduler: registry._context?.().scheduler
100
101
  }));
101
102
  });
102
103
  }
@@ -124,6 +125,8 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
124
125
  const registryCleanups = new Map();
125
126
  const runtimeContext = {};
126
127
  const boundEntries = new Set();
128
+ let subscriptionCounter = 0;
129
+ let effectCounter = 0;
127
130
 
128
131
  const registry = attachRegistryInspection({
129
132
  register(id, signalLike) {
@@ -199,17 +202,21 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
199
202
  return createRef(registry, id);
200
203
  },
201
204
 
202
- subscribe(path, fn) {
205
+ subscribe(path, fn, options = {}) {
203
206
  if (typeof fn !== "function") {
204
207
  throw new TypeError("subscribe(path, fn) requires a function.");
205
208
  }
206
209
  const parsed = parsePath(path, entries);
207
210
  const entry = requireEntry(entries, parsed.id);
211
+ const subscriptionId = ++subscriptionCounter;
208
212
  return entry.subscribe(() => {
209
- fn(registry.get(parsed.path), {
213
+ scheduleCallback(() => fn(registry.get(parsed.path), {
210
214
  id: parsed.id,
211
215
  path: parsed.path,
212
216
  signal: entry
217
+ }), {
218
+ ...options,
219
+ key: options.key ?? `signal:${parsed.path}:${subscriptionId}`
213
220
  });
214
221
  });
215
222
  },
@@ -227,10 +234,12 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
227
234
  return registry.ref(id);
228
235
  },
229
236
 
230
- effect(fn) {
237
+ effect(fn, options = {}) {
231
238
  let cleanup;
232
239
  let dependencyCleanups = [];
233
240
  let stopped = false;
241
+ const scheduler = options.scheduler;
242
+ const effectId = ++effectCounter;
234
243
 
235
244
  const run = () => {
236
245
  if (stopped) {
@@ -249,10 +258,22 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
249
258
  server: runtimeContext.server,
250
259
  router: runtimeContext.router,
251
260
  loader: runtimeContext.loader,
252
- cache: runtimeContext.cache
261
+ cache: runtimeContext.cache,
262
+ scheduler: runtimeContext.scheduler
253
263
  }));
254
264
  cleanup = outcome.value;
255
- dependencyCleanups = outcome.dependencies.map((dependency) => registry.subscribe(dependency, run));
265
+ dependencyCleanups = outcome.dependencies.map((dependency) => registry.subscribe(dependency, scheduleRun));
266
+ };
267
+
268
+ const scheduleRun = () => {
269
+ if (!scheduler) {
270
+ run();
271
+ return;
272
+ }
273
+ scheduler.enqueue(options.phase ?? "effect", run, {
274
+ scope: options.scope,
275
+ key: options.key ?? `effect:${effectId}`
276
+ });
256
277
  };
257
278
 
258
279
  run();
@@ -329,6 +350,17 @@ export function createSignalRegistry(initialMap = {}, options = {}) {
329
350
  registryCleanups.set(id, cleanup);
330
351
  }
331
352
  }
353
+
354
+ function scheduleCallback(fn, options = {}) {
355
+ const scheduler = options.scheduler;
356
+ if (!scheduler || options.phase === "sync") {
357
+ return fn();
358
+ }
359
+ return scheduler.enqueue(options.phase ?? "effect", fn, {
360
+ scope: options.scope,
361
+ key: options.key
362
+ });
363
+ }
332
364
  }
333
365
 
334
366
  function normalizeSignal(signalLike) {