@async/framework 0.11.5 → 0.11.7

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/browser.umd.js CHANGED
@@ -30,8 +30,9 @@
30
30
  let version = 0;
31
31
  let registry;
32
32
  let registeredId = id;
33
- let activeController;
34
- let activeAbort;
33
+ let activeRun;
34
+ let executionToken = 0;
35
+ let disposed = false;
35
36
  const subscribers = new Set();
36
37
  const dependencyCleanups = new Set();
37
38
 
@@ -73,109 +74,62 @@
73
74
  },
74
75
 
75
76
  refresh() {
76
- if (!registry) {
77
+ if (!registry || disposed) {
77
78
  throw new Error(`Async signal "${registeredId}" is not registered.`);
78
79
  }
79
80
 
80
- if (activeAbort && !activeAbort.aborted) {
81
- activeAbort.cancel(new Error(`Async signal "${registeredId}" refreshed.`));
82
- }
81
+ cancelRun(activeRun, new Error(`Async signal "${registeredId}" refreshed.`));
83
82
 
83
+ const runRegistry = registry;
84
+ const runId = registeredId;
84
85
  const runVersion = version + 1;
85
86
  version = runVersion;
86
87
  loading = true;
87
88
  error = null;
88
89
  status = "loading";
89
90
 
90
- const controller = new AbortController();
91
- activeController = controller;
92
- activeAbort = controller.signal;
93
- attachCancel(activeAbort, controller);
91
+ const run = createRun(runRegistry, runId, runVersion);
92
+ activeRun = run;
94
93
  notify();
95
94
 
96
- const context = {
97
- signals: registry,
98
- id: registeredId,
99
- get server() {
100
- const context = registry._context?.() ?? {};
101
- const server = context.server;
102
- if (typeof server?._withContext === "function") {
103
- return server._withContext({
104
- signals: registry,
105
- router: context.router,
106
- loader: context.loader,
107
- cache: context.cache,
108
- abort: activeAbort,
109
- scheduler: context.scheduler
110
- });
111
- }
112
- return server;
113
- },
114
- get router() {
115
- return registry._context?.().router;
116
- },
117
- get loader() {
118
- return registry._context?.().loader;
119
- },
120
- get cache() {
121
- return registry._context?.().cache;
122
- },
123
- get scheduler() {
124
- return registry._context?.().scheduler;
125
- },
126
- get version() {
127
- return runVersion;
128
- },
129
- get abort() {
130
- return activeAbort;
131
- },
132
- refresh() {
133
- return state.refresh();
134
- }
135
- };
95
+ const context = createRunContext(run);
136
96
 
137
97
  let outcome;
138
98
  try {
139
- outcome = registry._collectDependencies(() => fn.call(context));
99
+ outcome = runRegistry._collectDependencies(() => fn.call(context));
140
100
  } catch (cause) {
141
- finishError(runVersion, cause);
101
+ finishError(run, cause);
142
102
  return Promise.reject(cause);
143
103
  }
144
104
 
145
- syncDependencies(outcome.dependencies);
105
+ syncDependencies(outcome.dependencies, run);
146
106
 
147
107
  return Promise.resolve(outcome.value).then(
148
108
  (nextValue) => {
149
- if (!isCurrent(runVersion)) {
109
+ if (!isRunCurrent(run)) {
150
110
  return value;
151
111
  }
152
112
  value = nextValue;
153
113
  loading = false;
154
114
  error = null;
155
115
  status = "ready";
116
+ activeRun = undefined;
156
117
  notify();
157
118
  return value;
158
119
  },
159
120
  (cause) => {
160
- if (!isCurrent(runVersion)) {
121
+ if (!isRunCurrent(run)) {
161
122
  return value;
162
123
  }
163
- if (activeAbort?.aborted) {
164
- loading = false;
165
- status = value === undefined ? "idle" : "ready";
166
- notify();
167
- return value;
168
- }
169
- finishError(runVersion, cause);
124
+ finishError(run, cause);
170
125
  return value;
171
126
  }
172
127
  );
173
128
  },
174
129
 
175
130
  cancel(reason) {
176
- if (activeAbort && !activeAbort.aborted) {
177
- activeAbort.cancel(reason);
178
- }
131
+ cancelCurrentRun(reason, { settle: true, notifyChange: true });
132
+ return value;
179
133
  },
180
134
 
181
135
  subscribe(fn) {
@@ -204,9 +158,7 @@
204
158
  if (!isAsyncSignalSnapshot(snapshot)) {
205
159
  return state.set(snapshot);
206
160
  }
207
- if (activeAbort && !activeAbort.aborted) {
208
- activeAbort.cancel(new Error(`Async signal "${registeredId}" restored from snapshot.`));
209
- }
161
+ cancelCurrentRun(new Error(`Async signal "${registeredId}" restored from snapshot.`));
210
162
  value = snapshot.value;
211
163
  loading = Boolean(snapshot.loading);
212
164
  error = snapshot.error ?? null;
@@ -222,7 +174,7 @@
222
174
  registry = nextRegistry;
223
175
  registeredId = nextId;
224
176
  const start = () => {
225
- if (registry === nextRegistry && status === "idle") {
177
+ if (!disposed && registry === nextRegistry && status === "idle") {
226
178
  state.refresh();
227
179
  }
228
180
  };
@@ -238,30 +190,161 @@
238
190
  },
239
191
 
240
192
  _dispose() {
241
- state.cancel(new Error(`Async signal "${registeredId}" disposed.`));
193
+ if (disposed) {
194
+ return;
195
+ }
196
+ disposed = true;
197
+ cancelQueuedWork();
198
+ cancelCurrentRun(new Error(`Async signal "${registeredId}" disposed.`));
242
199
  for (const cleanup of dependencyCleanups) {
243
200
  cleanup();
244
201
  }
245
202
  dependencyCleanups.clear();
246
203
  subscribers.clear();
204
+ registry = undefined;
247
205
  }
248
206
  };
249
207
 
250
- function finishError(runVersion, cause) {
251
- if (!isCurrent(runVersion)) {
208
+ function createRun(runRegistry, runId, runVersion) {
209
+ const controller = new AbortController();
210
+ const abort = controller.signal;
211
+ const runContext = captureRunContext(runRegistry, abort);
212
+ const run = {
213
+ token: ++executionToken,
214
+ version: runVersion,
215
+ registry: runRegistry,
216
+ id: runId,
217
+ controller,
218
+ abort,
219
+ ...runContext
220
+ };
221
+ attachCancel(abort, controller, (reason) => {
222
+ cancelRun(run, reason, { settle: true, notifyChange: true });
223
+ });
224
+ return run;
225
+ }
226
+
227
+ function captureRunContext(runRegistry, abort) {
228
+ const context = runRegistry._context?.() ?? {};
229
+ const serverContext = {
230
+ signals: runRegistry,
231
+ router: context.router,
232
+ loader: context.loader,
233
+ cache: context.cache,
234
+ abort,
235
+ scheduler: context.scheduler
236
+ };
237
+ const server = typeof context.server?._withContext === "function"
238
+ ? context.server._withContext(serverContext)
239
+ : context.server;
240
+
241
+ return {
242
+ signals: runRegistry,
243
+ server,
244
+ router: context.router,
245
+ loader: context.loader,
246
+ cache: context.cache,
247
+ scheduler: context.scheduler
248
+ };
249
+ }
250
+
251
+ function createRunContext(run) {
252
+ return {
253
+ signals: run.signals,
254
+ id: run.id,
255
+ get server() {
256
+ return run.server;
257
+ },
258
+ get router() {
259
+ return run.router;
260
+ },
261
+ get loader() {
262
+ return run.loader;
263
+ },
264
+ get cache() {
265
+ return run.cache;
266
+ },
267
+ get scheduler() {
268
+ return run.scheduler;
269
+ },
270
+ get version() {
271
+ return run.version;
272
+ },
273
+ get abort() {
274
+ return run.abort;
275
+ },
276
+ refresh() {
277
+ return state.refresh();
278
+ }
279
+ };
280
+ }
281
+
282
+ function finishError(run, cause) {
283
+ if (!isRunCurrent(run)) {
252
284
  return;
253
285
  }
254
286
  loading = false;
255
287
  error = cause;
256
288
  status = "error";
289
+ activeRun = undefined;
257
290
  notify();
258
291
  }
259
292
 
260
- function isCurrent(runVersion) {
261
- return runVersion === version && activeController?.signal === activeAbort;
293
+ function isRunCurrent(run) {
294
+ return Boolean(run)
295
+ && !disposed
296
+ && activeRun === run
297
+ && run.token === executionToken
298
+ && run.registry === registry
299
+ && run.id === registeredId
300
+ && !run.abort.aborted;
301
+ }
302
+
303
+ function cancelCurrentRun(reason, options = {}) {
304
+ const run = activeRun;
305
+ const shouldSettle = Boolean(run) || loading;
306
+ if (run) {
307
+ cancelRun(run, reason);
308
+ } else if (shouldSettle) {
309
+ executionToken += 1;
310
+ }
311
+ if (options.settle && shouldSettle && !disposed) {
312
+ settleCanceled(options.notifyChange);
313
+ }
314
+ }
315
+
316
+ function cancelRun(run, reason, options = {}) {
317
+ if (!run) {
318
+ return;
319
+ }
320
+ const wasActive = activeRun === run;
321
+ if (wasActive) {
322
+ executionToken += 1;
323
+ activeRun = undefined;
324
+ }
325
+ if (!run.abort.aborted) {
326
+ run.controller.abort(reason);
327
+ }
328
+ if (wasActive && options.settle && !disposed) {
329
+ settleCanceled(options.notifyChange);
330
+ }
331
+ }
332
+
333
+ function settleCanceled(notifyChange = false) {
334
+ const nextStatus = value === undefined ? "idle" : "ready";
335
+ const changed = loading || error !== null || status !== nextStatus;
336
+ loading = false;
337
+ error = null;
338
+ status = nextStatus;
339
+ if (notifyChange && changed) {
340
+ notify();
341
+ }
262
342
  }
263
343
 
264
- function syncDependencies(dependencies) {
344
+ function syncDependencies(dependencies, run) {
345
+ if (!isRunCurrent(run)) {
346
+ return;
347
+ }
265
348
  for (const cleanup of dependencyCleanups) {
266
349
  cleanup();
267
350
  }
@@ -270,15 +353,16 @@
270
353
  for (const dependency of dependencies) {
271
354
  const dependencyId = String(dependency).split(".")[0];
272
355
  if (dependencyId && dependencyId !== registeredId) {
273
- dependencyCleanups.add(registry.subscribe(dependency, () => scheduleRefresh()));
356
+ dependencyCleanups.add(run.registry.subscribe(dependency, () => scheduleRefresh()));
274
357
  }
275
358
  }
276
359
  }
277
360
 
278
361
  function scheduleRefresh() {
279
- if (activeAbort && !activeAbort.aborted) {
280
- activeAbort.cancel(new Error(`Async signal "${registeredId}" dependency changed.`));
362
+ if (disposed || !registry) {
363
+ return;
281
364
  }
365
+ cancelRun(activeRun, new Error(`Async signal "${registeredId}" dependency changed.`));
282
366
  const scheduler = registry?._context?.().scheduler;
283
367
  if (!scheduler) {
284
368
  state.refresh();
@@ -290,6 +374,11 @@
290
374
  });
291
375
  }
292
376
 
377
+ function cancelQueuedWork() {
378
+ const scheduler = registry?._context?.().scheduler;
379
+ scheduler?.cancelScope?.(registeredId);
380
+ }
381
+
293
382
  function notify() {
294
383
  for (const subscriber of [...subscribers]) {
295
384
  subscriber(state);
@@ -324,11 +413,15 @@
324
413
  return value === undefined ? "idle" : "ready";
325
414
  }
326
415
 
327
- function attachCancel(signal, controller) {
416
+ function attachCancel(signal, controller, onCancel) {
328
417
  Object.defineProperty(signal, "cancel", {
329
418
  configurable: true,
330
419
  enumerable: false,
331
420
  value(reason) {
421
+ if (typeof onCancel === "function") {
422
+ onCancel(reason);
423
+ return;
424
+ }
332
425
  controller.abort(reason);
333
426
  }
334
427
  });
@@ -2296,9 +2389,10 @@
2296
2389
  })();
2297
2390
 
2298
2391
  const __serverModule = (() => {
2299
- const serverEnvelopeKeys = new Set(["value", "signals", "boundary", "html", "redirect", "error"]);
2392
+ const serverEnvelopeKind = Symbol.for("@async/framework.serverResult");
2393
+ const serverEnvelopeWireKey = "__async_server_result__";
2394
+ const serverEnvelopeWireVersion = 1;
2300
2395
  const appliedServerResult = Symbol.for("@async/framework.appliedServerResult");
2301
- const appliedServerValues = new WeakSet();
2302
2396
 
2303
2397
  function createServerProxy({
2304
2398
  endpoint = "/__async/server",
@@ -2341,9 +2435,7 @@
2341
2435
  throw new Error(`Server function "${id}" failed with ${response.status}.`);
2342
2436
  }
2343
2437
 
2344
- const result = await readServerResponse(id, response);
2345
- await applyServerResult(result, runContext);
2346
- return markAppliedServerValue(unwrapServerResult(result));
2438
+ return consumeServerResult(await readServerResponse(id, response), runContext);
2347
2439
  }
2348
2440
 
2349
2441
  return createServerNamespace(run, {
@@ -2378,7 +2470,7 @@
2378
2470
  if (!isServerEnvelope(result)) {
2379
2471
  return result;
2380
2472
  }
2381
- if (result[appliedServerResult] || appliedServerValues.has(result)) {
2473
+ if (result[appliedServerResult]) {
2382
2474
  return result;
2383
2475
  }
2384
2476
 
@@ -2410,18 +2502,16 @@
2410
2502
  return result;
2411
2503
  }
2412
2504
 
2413
- function unwrapServerResult(result) {
2414
- if (isServerEnvelope(result) && Object.hasOwn(result, "value")) {
2415
- return result.value;
2416
- }
2417
- return result;
2505
+ async function consumeServerResult(result, context = {}) {
2506
+ await applyServerResult(result, context);
2507
+ return unwrapServerResult(result);
2418
2508
  }
2419
2509
 
2420
- function markAppliedServerValue(value) {
2421
- if (value && typeof value === "object") {
2422
- appliedServerValues.add(value);
2510
+ function unwrapServerResult(result) {
2511
+ if (isServerEnvelope(result)) {
2512
+ return Object.hasOwn(result, "value") ? result.value : undefined;
2423
2513
  }
2424
- return value;
2514
+ return result;
2425
2515
  }
2426
2516
 
2427
2517
  function markAppliedServerResult(result) {
@@ -2465,9 +2555,7 @@
2465
2555
  throw new Error("Server namespace is not directly callable.");
2466
2556
  }
2467
2557
  const context = contextProvider() ?? {};
2468
- const result = await run(parts.join("."), args, context);
2469
- await applyServerResult(result, context);
2470
- return unwrapServerResult(result);
2558
+ return run(parts.join("."), args, context);
2471
2559
  };
2472
2560
 
2473
2561
  const proxy = new Proxy(callable, {
@@ -2524,7 +2612,7 @@
2524
2612
 
2525
2613
  async function readServerResponse(id, response) {
2526
2614
  if (response.status === 204) {
2527
- return { value: undefined };
2615
+ return undefined;
2528
2616
  }
2529
2617
  const type = response.headers.get("content-type") ?? "";
2530
2618
  if (type.includes("application/json")) {
@@ -2542,7 +2630,7 @@
2542
2630
  if (typeof response.text !== "function") {
2543
2631
  throw new Error(`Server function "${id}" transport returned an invalid response: missing text().`);
2544
2632
  }
2545
- return { value: await response.text() };
2633
+ return response.text();
2546
2634
  }
2547
2635
 
2548
2636
  function snapshotSignalPaths(paths = [], signals) {
@@ -2687,7 +2775,8 @@
2687
2775
  if (!value || typeof value !== "object" || Array.isArray(value)) {
2688
2776
  return false;
2689
2777
  }
2690
- return Object.keys(value).some((key) => serverEnvelopeKeys.has(key));
2778
+ return value[serverEnvelopeKind] === true
2779
+ || value[serverEnvelopeWireKey] === serverEnvelopeWireVersion;
2691
2780
  }
2692
2781
 
2693
2782
  function toError(value) {
@@ -2709,11 +2798,11 @@
2709
2798
  throw new TypeError("Server function id must be a non-empty string.");
2710
2799
  }
2711
2800
  }
2712
- return { createServerProxy, resolveServerCommandArguments, applyServerResult, unwrapServerResult, defaultInput, createServerNamespace, createSignalReader, assertServerId };
2801
+ return { createServerProxy, resolveServerCommandArguments, applyServerResult, consumeServerResult, unwrapServerResult, defaultInput, createServerNamespace, createSignalReader, assertServerId };
2713
2802
  })();
2714
2803
 
2715
2804
  const __handlersModule = (() => {
2716
- const { applyServerResult, defaultInput, resolveServerCommandArguments, unwrapServerResult } = __serverModule;
2805
+ const { defaultInput, resolveServerCommandArguments } = __serverModule;
2717
2806
  const { attachRegistryInspection, createRegistryStore } = __registryStoreModule;
2718
2807
  const { createLazyRegistry, isLazyDescriptor } = __lazyRegistryModule;
2719
2808
  const builtInTokens = new Set(["prevent", "preventDefault", "stopPropagation", "stopImmediatePropagation"]);
@@ -2811,8 +2900,7 @@
2811
2900
  signalPaths: resolved.signalPaths,
2812
2901
  signalValues: resolved.signalValues
2813
2902
  });
2814
- await applyServerResult(result, runContext);
2815
- results.push(unwrapServerResult(result));
2903
+ results.push(result);
2816
2904
  continue;
2817
2905
  }
2818
2906