@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/CHANGELOG.md +25 -0
- package/browser.js +196 -86
- package/browser.min.js +1 -1
- package/browser.ts +196 -86
- package/browser.umd.js +196 -86
- package/browser.umd.min.js +1 -1
- package/framework.ts +196 -86
- package/package.json +1 -1
- package/server.js +196 -86
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.11.5 - 2026-06-18
|
|
17
|
+
|
|
18
|
+
- Treated materialized signals and lazy async-signal descriptors as one
|
|
19
|
+
logical namespace so duplicate IDs are rejected consistently.
|
|
20
|
+
- Fixed lazy async-signal `unregister(...)` before and after materialization so
|
|
21
|
+
removed descriptors cannot rematerialize.
|
|
22
|
+
- Preserved reusable app async-signal declarations when materialized runtime
|
|
23
|
+
async state is destroyed.
|
|
24
|
+
- Bundle size from bundled TypeScript source: `browser.ts` 181,266 B raw /
|
|
25
|
+
33,853 B gzip -> `browser.min.js` 77,285 B raw / 22,868 B gzip
|
|
26
|
+
(-103,981 B raw, -10,985 B gzip).
|
|
27
|
+
|
|
3
28
|
## 0.11.4 - 2026-06-18
|
|
4
29
|
|
|
5
30
|
- Restored snapshot signal keys as exact IDs so dotted plain, async, and
|
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 (!
|
|
110
|
+
if (!isRunCurrent(run)) {
|
|
150
111
|
return value;
|
|
151
112
|
}
|
|
152
|
-
|
|
153
|
-
loading = false;
|
|
154
|
-
status = value === undefined ? "idle" : "ready";
|
|
155
|
-
notify();
|
|
156
|
-
return value;
|
|
157
|
-
}
|
|
158
|
-
finishError(runVersion, cause);
|
|
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;
|
|
290
|
+
}
|
|
291
|
+
|
|
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
|
+
}
|
|
251
303
|
}
|
|
252
304
|
|
|
253
|
-
function
|
|
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
|
});
|
|
@@ -1218,10 +1311,16 @@ const __signalsModule = (() => {
|
|
|
1218
1311
|
let subscriptionCounter = 0;
|
|
1219
1312
|
let effectCounter = 0;
|
|
1220
1313
|
|
|
1314
|
+
for (const id of entries.keys()) {
|
|
1315
|
+
if (asyncDescriptors.has(id)) {
|
|
1316
|
+
throw new Error(`Signal "${id}" is already registered.`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1221
1320
|
const registry = attachRegistryInspection({
|
|
1222
1321
|
register(id, signalLike) {
|
|
1223
1322
|
assertId(id);
|
|
1224
|
-
if (entries.has(id)) {
|
|
1323
|
+
if (entries.has(id) || asyncDescriptors.has(id)) {
|
|
1225
1324
|
throw new Error(`Signal "${id}" is already registered.`);
|
|
1226
1325
|
}
|
|
1227
1326
|
const entry = normalizeSignal(signalLike);
|
|
@@ -1239,14 +1338,19 @@ const __signalsModule = (() => {
|
|
|
1239
1338
|
|
|
1240
1339
|
unregister(id) {
|
|
1241
1340
|
assertId(id);
|
|
1242
|
-
|
|
1341
|
+
const hadEntry = entries.has(id);
|
|
1342
|
+
const hadDescriptor = asyncDescriptors.has(id);
|
|
1343
|
+
if (!hadEntry && !hadDescriptor) {
|
|
1243
1344
|
return false;
|
|
1244
1345
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1346
|
+
if (hadEntry) {
|
|
1347
|
+
registryCleanups.get(id)?.();
|
|
1348
|
+
registryCleanups.delete(id);
|
|
1349
|
+
entries.get(id)?._dispose?.();
|
|
1350
|
+
entries.delete(id);
|
|
1351
|
+
boundEntries.delete(id);
|
|
1352
|
+
}
|
|
1353
|
+
asyncDescriptors.delete(id);
|
|
1250
1354
|
return true;
|
|
1251
1355
|
},
|
|
1252
1356
|
|
|
@@ -1438,6 +1542,9 @@ const __signalsModule = (() => {
|
|
|
1438
1542
|
_adoptMany(map = {}) {
|
|
1439
1543
|
for (const [id, signalLike] of Object.entries(map ?? {})) {
|
|
1440
1544
|
if (!entries.has(id)) {
|
|
1545
|
+
if (asyncDescriptors.has(id)) {
|
|
1546
|
+
throw new Error(`Signal "${id}" is already registered.`);
|
|
1547
|
+
}
|
|
1441
1548
|
const entry = cloneSignalDeclaration(signalLike);
|
|
1442
1549
|
entries.set(id, entry);
|
|
1443
1550
|
bindEntry(id, entry);
|
|
@@ -5338,6 +5445,9 @@ const __appModule = (() => {
|
|
|
5338
5445
|
return;
|
|
5339
5446
|
}
|
|
5340
5447
|
for (const [id, value] of Object.entries(entries)) {
|
|
5448
|
+
if (type === "asyncSignal" && runtime.signals?.has?.(id) && !runtime.registry.has(type, id)) {
|
|
5449
|
+
throw new Error(`Signal "${id}" is already registered.`);
|
|
5450
|
+
}
|
|
5341
5451
|
registerSnapshotEntry(runtime.registry, type, id, value, options);
|
|
5342
5452
|
}
|
|
5343
5453
|
concreteRegistry?._adoptMany?.(entries);
|