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