@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/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)) {
161
- return value;
162
- }
163
- if (activeAbort?.aborted) {
164
- loading = false;
165
- status = value === undefined ? "idle" : "ready";
166
- notify();
121
+ if (!isRunCurrent(run)) {
167
122
  return value;
168
123
  }
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;
262
301
  }
263
302
 
264
- function syncDependencies(dependencies) {
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
+ }
342
+ }
343
+
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
  });