@async/framework 0.11.4 → 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)) {
113
+ if (!isRunCurrent(run)) {
153
114
  return value;
154
115
  }
155
- if (activeAbort?.aborted) {
156
- loading = false;
157
- status = value === undefined ? "idle" : "ready";
158
- notify();
159
- return value;
160
- }
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;
293
+ }
294
+
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
+ }
254
306
  }
255
307
 
256
- function syncDependencies(dependencies) {
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
  });
@@ -1221,10 +1314,16 @@ const __signalsModule = (() => {
1221
1314
  let subscriptionCounter = 0;
1222
1315
  let effectCounter = 0;
1223
1316
 
1317
+ for (const id of entries.keys()) {
1318
+ if (asyncDescriptors.has(id)) {
1319
+ throw new Error(`Signal "${id}" is already registered.`);
1320
+ }
1321
+ }
1322
+
1224
1323
  const registry = attachRegistryInspection({
1225
1324
  register(id, signalLike) {
1226
1325
  assertId(id);
1227
- if (entries.has(id)) {
1326
+ if (entries.has(id) || asyncDescriptors.has(id)) {
1228
1327
  throw new Error(`Signal "${id}" is already registered.`);
1229
1328
  }
1230
1329
  const entry = normalizeSignal(signalLike);
@@ -1242,14 +1341,19 @@ const __signalsModule = (() => {
1242
1341
 
1243
1342
  unregister(id) {
1244
1343
  assertId(id);
1245
- if (!entries.has(id)) {
1344
+ const hadEntry = entries.has(id);
1345
+ const hadDescriptor = asyncDescriptors.has(id);
1346
+ if (!hadEntry && !hadDescriptor) {
1246
1347
  return false;
1247
1348
  }
1248
- registryCleanups.get(id)?.();
1249
- registryCleanups.delete(id);
1250
- entries.get(id)?._dispose?.();
1251
- entries.delete(id);
1252
- boundEntries.delete(id);
1349
+ if (hadEntry) {
1350
+ registryCleanups.get(id)?.();
1351
+ registryCleanups.delete(id);
1352
+ entries.get(id)?._dispose?.();
1353
+ entries.delete(id);
1354
+ boundEntries.delete(id);
1355
+ }
1356
+ asyncDescriptors.delete(id);
1253
1357
  return true;
1254
1358
  },
1255
1359
 
@@ -1441,6 +1545,9 @@ const __signalsModule = (() => {
1441
1545
  _adoptMany(map = {}) {
1442
1546
  for (const [id, signalLike] of Object.entries(map ?? {})) {
1443
1547
  if (!entries.has(id)) {
1548
+ if (asyncDescriptors.has(id)) {
1549
+ throw new Error(`Signal "${id}" is already registered.`);
1550
+ }
1444
1551
  const entry = cloneSignalDeclaration(signalLike);
1445
1552
  entries.set(id, entry);
1446
1553
  bindEntry(id, entry);
@@ -5341,6 +5448,9 @@ const __appModule = (() => {
5341
5448
  return;
5342
5449
  }
5343
5450
  for (const [id, value] of Object.entries(entries)) {
5451
+ if (type === "asyncSignal" && runtime.signals?.has?.(id) && !runtime.registry.has(type, id)) {
5452
+ throw new Error(`Signal "${id}" is already registered.`);
5453
+ }
5344
5454
  registerSnapshotEntry(runtime.registry, type, id, value, options);
5345
5455
  }
5346
5456
  concreteRegistry?._adoptMany?.(entries);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@async/framework",
3
- "version": "0.11.4",
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",