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