@async/framework 0.11.5 → 0.11.6

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/framework.ts CHANGED
@@ -22,8 +22,9 @@ const __asyncSignalModule = (() => {
22
22
  let version = 0;
23
23
  let registry;
24
24
  let registeredId = id;
25
- let activeController;
26
- let activeAbort;
25
+ let activeRun;
26
+ let executionToken = 0;
27
+ let disposed = false;
27
28
  const subscribers = new Set();
28
29
  const dependencyCleanups = new Set();
29
30
 
@@ -65,109 +66,62 @@ const __asyncSignalModule = (() => {
65
66
  },
66
67
 
67
68
  refresh() {
68
- if (!registry) {
69
+ if (!registry || disposed) {
69
70
  throw new Error(`Async signal "${registeredId}" is not registered.`);
70
71
  }
71
72
 
72
- if (activeAbort && !activeAbort.aborted) {
73
- activeAbort.cancel(new Error(`Async signal "${registeredId}" refreshed.`));
74
- }
73
+ cancelRun(activeRun, new Error(`Async signal "${registeredId}" refreshed.`));
75
74
 
75
+ const runRegistry = registry;
76
+ const runId = registeredId;
76
77
  const runVersion = version + 1;
77
78
  version = runVersion;
78
79
  loading = true;
79
80
  error = null;
80
81
  status = "loading";
81
82
 
82
- const controller = new AbortController();
83
- activeController = controller;
84
- activeAbort = controller.signal;
85
- attachCancel(activeAbort, controller);
83
+ const run = createRun(runRegistry, runId, runVersion);
84
+ activeRun = run;
86
85
  notify();
87
86
 
88
- const context = {
89
- signals: registry,
90
- id: registeredId,
91
- get server() {
92
- const context = registry._context?.() ?? {};
93
- const server = context.server;
94
- if (typeof server?._withContext === "function") {
95
- return server._withContext({
96
- signals: registry,
97
- router: context.router,
98
- loader: context.loader,
99
- cache: context.cache,
100
- abort: activeAbort,
101
- scheduler: context.scheduler
102
- });
103
- }
104
- return server;
105
- },
106
- get router() {
107
- return registry._context?.().router;
108
- },
109
- get loader() {
110
- return registry._context?.().loader;
111
- },
112
- get cache() {
113
- return registry._context?.().cache;
114
- },
115
- get scheduler() {
116
- return registry._context?.().scheduler;
117
- },
118
- get version() {
119
- return runVersion;
120
- },
121
- get abort() {
122
- return activeAbort;
123
- },
124
- refresh() {
125
- return state.refresh();
126
- }
127
- };
87
+ const context = createRunContext(run);
128
88
 
129
89
  let outcome;
130
90
  try {
131
- outcome = registry._collectDependencies(() => fn.call(context));
91
+ outcome = runRegistry._collectDependencies(() => fn.call(context));
132
92
  } catch (cause) {
133
- finishError(runVersion, cause);
93
+ finishError(run, cause);
134
94
  return Promise.reject(cause);
135
95
  }
136
96
 
137
- syncDependencies(outcome.dependencies);
97
+ syncDependencies(outcome.dependencies, run);
138
98
 
139
99
  return Promise.resolve(outcome.value).then(
140
100
  (nextValue) => {
141
- if (!isCurrent(runVersion)) {
101
+ if (!isRunCurrent(run)) {
142
102
  return value;
143
103
  }
144
104
  value = nextValue;
145
105
  loading = false;
146
106
  error = null;
147
107
  status = "ready";
108
+ activeRun = undefined;
148
109
  notify();
149
110
  return value;
150
111
  },
151
112
  (cause) => {
152
- if (!isCurrent(runVersion)) {
153
- return value;
154
- }
155
- if (activeAbort?.aborted) {
156
- loading = false;
157
- status = value === undefined ? "idle" : "ready";
158
- notify();
113
+ if (!isRunCurrent(run)) {
159
114
  return value;
160
115
  }
161
- finishError(runVersion, cause);
116
+ finishError(run, cause);
162
117
  return value;
163
118
  }
164
119
  );
165
120
  },
166
121
 
167
122
  cancel(reason) {
168
- if (activeAbort && !activeAbort.aborted) {
169
- activeAbort.cancel(reason);
170
- }
123
+ cancelCurrentRun(reason, { settle: true, notifyChange: true });
124
+ return value;
171
125
  },
172
126
 
173
127
  subscribe(fn) {
@@ -196,9 +150,7 @@ const __asyncSignalModule = (() => {
196
150
  if (!isAsyncSignalSnapshot(snapshot)) {
197
151
  return state.set(snapshot);
198
152
  }
199
- if (activeAbort && !activeAbort.aborted) {
200
- activeAbort.cancel(new Error(`Async signal "${registeredId}" restored from snapshot.`));
201
- }
153
+ cancelCurrentRun(new Error(`Async signal "${registeredId}" restored from snapshot.`));
202
154
  value = snapshot.value;
203
155
  loading = Boolean(snapshot.loading);
204
156
  error = snapshot.error ?? null;
@@ -214,7 +166,7 @@ const __asyncSignalModule = (() => {
214
166
  registry = nextRegistry;
215
167
  registeredId = nextId;
216
168
  const start = () => {
217
- if (registry === nextRegistry && status === "idle") {
169
+ if (!disposed && registry === nextRegistry && status === "idle") {
218
170
  state.refresh();
219
171
  }
220
172
  };
@@ -230,30 +182,161 @@ const __asyncSignalModule = (() => {
230
182
  },
231
183
 
232
184
  _dispose() {
233
- state.cancel(new Error(`Async signal "${registeredId}" disposed.`));
185
+ if (disposed) {
186
+ return;
187
+ }
188
+ disposed = true;
189
+ cancelQueuedWork();
190
+ cancelCurrentRun(new Error(`Async signal "${registeredId}" disposed.`));
234
191
  for (const cleanup of dependencyCleanups) {
235
192
  cleanup();
236
193
  }
237
194
  dependencyCleanups.clear();
238
195
  subscribers.clear();
196
+ registry = undefined;
239
197
  }
240
198
  };
241
199
 
242
- function finishError(runVersion, cause) {
243
- if (!isCurrent(runVersion)) {
200
+ function createRun(runRegistry, runId, runVersion) {
201
+ const controller = new AbortController();
202
+ const abort = controller.signal;
203
+ const runContext = captureRunContext(runRegistry, abort);
204
+ const run = {
205
+ token: ++executionToken,
206
+ version: runVersion,
207
+ registry: runRegistry,
208
+ id: runId,
209
+ controller,
210
+ abort,
211
+ ...runContext
212
+ };
213
+ attachCancel(abort, controller, (reason) => {
214
+ cancelRun(run, reason, { settle: true, notifyChange: true });
215
+ });
216
+ return run;
217
+ }
218
+
219
+ function captureRunContext(runRegistry, abort) {
220
+ const context = runRegistry._context?.() ?? {};
221
+ const serverContext = {
222
+ signals: runRegistry,
223
+ router: context.router,
224
+ loader: context.loader,
225
+ cache: context.cache,
226
+ abort,
227
+ scheduler: context.scheduler
228
+ };
229
+ const server = typeof context.server?._withContext === "function"
230
+ ? context.server._withContext(serverContext)
231
+ : context.server;
232
+
233
+ return {
234
+ signals: runRegistry,
235
+ server,
236
+ router: context.router,
237
+ loader: context.loader,
238
+ cache: context.cache,
239
+ scheduler: context.scheduler
240
+ };
241
+ }
242
+
243
+ function createRunContext(run) {
244
+ return {
245
+ signals: run.signals,
246
+ id: run.id,
247
+ get server() {
248
+ return run.server;
249
+ },
250
+ get router() {
251
+ return run.router;
252
+ },
253
+ get loader() {
254
+ return run.loader;
255
+ },
256
+ get cache() {
257
+ return run.cache;
258
+ },
259
+ get scheduler() {
260
+ return run.scheduler;
261
+ },
262
+ get version() {
263
+ return run.version;
264
+ },
265
+ get abort() {
266
+ return run.abort;
267
+ },
268
+ refresh() {
269
+ return state.refresh();
270
+ }
271
+ };
272
+ }
273
+
274
+ function finishError(run, cause) {
275
+ if (!isRunCurrent(run)) {
244
276
  return;
245
277
  }
246
278
  loading = false;
247
279
  error = cause;
248
280
  status = "error";
281
+ activeRun = undefined;
249
282
  notify();
250
283
  }
251
284
 
252
- function isCurrent(runVersion) {
253
- return runVersion === version && activeController?.signal === activeAbort;
285
+ function isRunCurrent(run) {
286
+ return Boolean(run)
287
+ && !disposed
288
+ && activeRun === run
289
+ && run.token === executionToken
290
+ && run.registry === registry
291
+ && run.id === registeredId
292
+ && !run.abort.aborted;
254
293
  }
255
294
 
256
- function syncDependencies(dependencies) {
295
+ function cancelCurrentRun(reason, options = {}) {
296
+ const run = activeRun;
297
+ const shouldSettle = Boolean(run) || loading;
298
+ if (run) {
299
+ cancelRun(run, reason);
300
+ } else if (shouldSettle) {
301
+ executionToken += 1;
302
+ }
303
+ if (options.settle && shouldSettle && !disposed) {
304
+ settleCanceled(options.notifyChange);
305
+ }
306
+ }
307
+
308
+ function cancelRun(run, reason, options = {}) {
309
+ if (!run) {
310
+ return;
311
+ }
312
+ const wasActive = activeRun === run;
313
+ if (wasActive) {
314
+ executionToken += 1;
315
+ activeRun = undefined;
316
+ }
317
+ if (!run.abort.aborted) {
318
+ run.controller.abort(reason);
319
+ }
320
+ if (wasActive && options.settle && !disposed) {
321
+ settleCanceled(options.notifyChange);
322
+ }
323
+ }
324
+
325
+ function settleCanceled(notifyChange = false) {
326
+ const nextStatus = value === undefined ? "idle" : "ready";
327
+ const changed = loading || error !== null || status !== nextStatus;
328
+ loading = false;
329
+ error = null;
330
+ status = nextStatus;
331
+ if (notifyChange && changed) {
332
+ notify();
333
+ }
334
+ }
335
+
336
+ function syncDependencies(dependencies, run) {
337
+ if (!isRunCurrent(run)) {
338
+ return;
339
+ }
257
340
  for (const cleanup of dependencyCleanups) {
258
341
  cleanup();
259
342
  }
@@ -262,15 +345,16 @@ const __asyncSignalModule = (() => {
262
345
  for (const dependency of dependencies) {
263
346
  const dependencyId = String(dependency).split(".")[0];
264
347
  if (dependencyId && dependencyId !== registeredId) {
265
- dependencyCleanups.add(registry.subscribe(dependency, () => scheduleRefresh()));
348
+ dependencyCleanups.add(run.registry.subscribe(dependency, () => scheduleRefresh()));
266
349
  }
267
350
  }
268
351
  }
269
352
 
270
353
  function scheduleRefresh() {
271
- if (activeAbort && !activeAbort.aborted) {
272
- activeAbort.cancel(new Error(`Async signal "${registeredId}" dependency changed.`));
354
+ if (disposed || !registry) {
355
+ return;
273
356
  }
357
+ cancelRun(activeRun, new Error(`Async signal "${registeredId}" dependency changed.`));
274
358
  const scheduler = registry?._context?.().scheduler;
275
359
  if (!scheduler) {
276
360
  state.refresh();
@@ -282,6 +366,11 @@ const __asyncSignalModule = (() => {
282
366
  });
283
367
  }
284
368
 
369
+ function cancelQueuedWork() {
370
+ const scheduler = registry?._context?.().scheduler;
371
+ scheduler?.cancelScope?.(registeredId);
372
+ }
373
+
285
374
  function notify() {
286
375
  for (const subscriber of [...subscribers]) {
287
376
  subscriber(state);
@@ -316,11 +405,15 @@ const __asyncSignalModule = (() => {
316
405
  return value === undefined ? "idle" : "ready";
317
406
  }
318
407
 
319
- function attachCancel(signal, controller) {
408
+ function attachCancel(signal, controller, onCancel) {
320
409
  Object.defineProperty(signal, "cancel", {
321
410
  configurable: true,
322
411
  enumerable: false,
323
412
  value(reason) {
413
+ if (typeof onCancel === "function") {
414
+ onCancel(reason);
415
+ return;
416
+ }
324
417
  controller.abort(reason);
325
418
  }
326
419
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@async/framework",
3
- "version": "0.11.5",
3
+ "version": "0.11.6",
4
4
  "description": "No-build Loader app runtime with browser and server entrypoints, signals, command events, route partials, cache split, SSR activation, and streaming boundaries.",
5
5
  "type": "module",
6
6
  "main": "./server.js",