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