nginx-lint-plugin 0.10.1 → 0.10.2

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.
@@ -1,115 +1,150 @@
1
1
  "use jco";
2
2
 
3
+ function promiseWithResolvers() {
4
+ if (Promise.withResolvers) {
5
+ return Promise.withResolvers();
6
+ } else {
7
+ let resolve;
8
+ let reject;
9
+ const promise = new Promise((res, rej) => {
10
+ resolve = res;
11
+ reject = rej;
12
+ });
13
+ return { promise, resolve, reject };
14
+ }
15
+ }
16
+
3
17
  const _debugLog = (...args) => {
4
18
  if (!globalThis?.process?.env?.JCO_DEBUG) { return; }
5
19
  console.debug(...args);
6
- }
20
+ };
7
21
  const ASYNC_DETERMINISM = 'random';
22
+ const GLOBAL_COMPONENT_MEMORY_MAP = new Map();
23
+ const CURRENT_TASK_META = {};
8
24
 
9
- class GlobalComponentAsyncLowers {
10
- static map = new Map();
11
-
12
- constructor() { throw new Error('GlobalComponentAsyncLowers should not be constructed'); }
13
-
14
- static define(args) {
15
- const { componentIdx, qualifiedImportFn, fn } = args;
16
- let inner = GlobalComponentAsyncLowers.map.get(componentIdx);
17
- if (!inner) {
18
- inner = new Map();
19
- GlobalComponentAsyncLowers.map.set(componentIdx, inner);
25
+ function _getGlobalCurrentTaskMeta(componentIdx) {
26
+ const v = CURRENT_TASK_META[componentIdx];
27
+ if (v === undefined) { return v; }
28
+ return { ...v };
29
+ }
30
+
31
+ function _setGlobalCurrentTaskMeta(args) {
32
+ if (!args) { throw new TypeError('args missing'); }
33
+ if (args.taskID === undefined) { throw new TypeError('missing task ID'); }
34
+ if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); }
35
+ const { taskID, componentIdx } = args;
36
+ return CURRENT_TASK_META[componentIdx] = { taskID, componentIdx };
37
+ }
38
+
39
+ function _withGlobalCurrentTaskMeta(args) {
40
+ _debugLog('[_withGlobalCurrentTaskMeta()] args', args);
41
+ if (!args) { throw new TypeError('args missing'); }
42
+ if (args.taskID === undefined) { throw new TypeError('missing task ID'); }
43
+ if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); }
44
+ if (!args.fn) { throw new TypeError('missing fn'); }
45
+ const { taskID, componentIdx, fn } = args;
46
+
47
+ try {
48
+ CURRENT_TASK_META[componentIdx] = { taskID, componentIdx };
49
+ return fn();
50
+ } catch (err) {
51
+ _debugLog("error while executing sync callee/callback", {
52
+ ...args,
53
+ err,
54
+ });
55
+ throw err;
56
+ } finally {
57
+ CURRENT_TASK_META[componentIdx] = null;
58
+ }
59
+ }
60
+
61
+ async function _withGlobalCurrentTaskMetaAsync(args) {
62
+ _debugLog('[_withGlobalCurrentTaskMetaAsync()] args', args);
63
+ if (!args) { throw new TypeError('args missing'); }
64
+ if (args.taskID === undefined) { throw new TypeError('missing task ID'); }
65
+ if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); }
66
+ if (!args.fn) { throw new TypeError('missing fn'); }
67
+ const { taskID, componentIdx, fn } = args;
68
+
69
+ // If there is already an async task executing, we must wait for it
70
+ // to complete before we can can run the closure we were given
71
+ //
72
+ let current = CURRENT_TASK_META[componentIdx];
73
+ let cstate;
74
+ if (current && current.taskID !== taskID) {
75
+ cstate = getOrCreateAsyncState(componentIdx);
76
+ while (current && current.taskID !== taskID) {
77
+ const { promise, resolve } = Promise.withResolvers();
78
+ cstate.onNextExclusiveRelease(resolve);
79
+ await promise;
80
+ current = CURRENT_TASK_META[componentIdx];
20
81
  }
21
82
 
22
- inner.set(qualifiedImportFn, fn);
83
+ // Since we've just waited for the component to not be locked, re-lock
84
+ // exclusivity so we can run the fn below (likely a callee/callback)
85
+ cstate.exclusiveLock();
23
86
  }
24
87
 
25
- static lookup(componentIdx, qualifiedImportFn) {
26
- let inner = GlobalComponentAsyncLowers.map.get(componentIdx);
27
- if (!inner) {
28
- inner = new Map();
29
- GlobalComponentAsyncLowers.map.set(componentIdx, inner);
30
- }
31
-
32
- const found = inner.get(qualifiedImportFn);
33
- if (found) { return found; }
34
-
35
- // In some cases, async lowers are *not* host provided, and
36
- // but contain/will call an async function in the host.
37
- //
38
- // One such case is `stream.write`/`stream.read` trampolines which are
39
- // actually re-exported through a patch up container *before*
40
- // they call the relevant async host trampoline.
41
- //
42
- // So the path of execution from a component export would be:
43
- //
44
- // async guest export --> stream.write import (host wired) -> guest export (patch component) -> async host trampoline
45
- //
46
- // On top of all this, the trampoline that is eventually called is async,
47
- // so we must await the patched guest export call.
48
- //
49
- if (qualifiedImportFn.includes("[stream-write-") || qualifiedImportFn.includes("[stream-read-")) {
50
- return async (...args) => {
51
- const [originalFn, ...params] = args;
52
- return await originalFn(...params);
53
- };
54
- }
55
-
56
- // All other cases can call the registered function directly
57
- return (...args) => {
58
- const [originalFn, ...params] = args;
59
- return originalFn(...params);
60
- };
88
+ try {
89
+ CURRENT_TASK_META[componentIdx] = { taskID, componentIdx };
90
+ return await fn();
91
+ } catch (err) {
92
+ _debugLog("error while executing async callee/callback", {
93
+ ...args,
94
+ err,
95
+ });
96
+ throw err;
97
+ } finally {
98
+ CURRENT_TASK_META[componentIdx] = null;
61
99
  }
62
100
  }
63
101
 
64
- class GlobalAsyncParamLowers {
65
- static map = new Map();
102
+ async function _clearCurrentTask(args) {
103
+ _debugLog('[_clearCurrentTask()] args', args);
104
+ if (!args) { throw new TypeError('args missing'); }
105
+ if (args.taskID === undefined) { throw new TypeError('missing task ID'); }
106
+ if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); }
107
+ const { taskID, componentIdx } = args;
66
108
 
67
- static generateKey(args) {
68
- const { componentIdx, iface, fnName } = args;
69
- if (componentIdx === undefined) { throw new TypeError("missing component idx"); }
70
- if (iface === undefined) { throw new TypeError("missing iface name"); }
71
- if (fnName === undefined) { throw new TypeError("missing function name"); }
72
- return `${componentIdx}-${iface}-${fnName}`;
73
- }
109
+ const meta = CURRENT_TASK_META[componentIdx];
110
+ if (!meta) { throw new Error(`missing current task meta for component idx [${componentIdx}]`); }
74
111
 
75
- static define(args) {
76
- const { componentIdx, iface, fnName, fn } = args;
77
- if (!fn) { throw new TypeError('missing function'); }
78
- const key = GlobalAsyncParamLowers.generateKey(args);
79
- GlobalAsyncParamLowers.map.set(key, fn);
112
+ if (meta.taskID !== taskID) {
113
+ throw new Error(`task ID [${meta.taskID}] != requested ID [${taskID}]`);
80
114
  }
81
-
82
- static lookup(args) {
83
- const { componentIdx, iface, fnName } = args;
84
- const key = GlobalAsyncParamLowers.generateKey(args);
85
- return GlobalAsyncParamLowers.map.get(key);
115
+ if (meta.componentIdx !== componentIdx) {
116
+ throw new Error(`component idx [${meta.componentIdx}] != requested idx [${componentIdx}]`);
86
117
  }
118
+
119
+ CURRENT_TASK_META[componentIdx] = null;
87
120
  }
88
121
 
89
- class GlobalComponentMemories {
90
- static map = new Map();
122
+ function lookupMemoriesForComponent(args) {
123
+ const { componentIdx } = args ?? {};
124
+ if (args.componentIdx === undefined) { throw new TypeError("missing component idx"); }
91
125
 
92
- constructor() { throw new Error('GlobalComponentMemories should not be constructed'); }
126
+ const metas = GLOBAL_COMPONENT_MEMORY_MAP.get(componentIdx);
127
+ if (!metas) { return []; }
93
128
 
94
- static save(args) {
95
- const { idx, componentIdx, memory } = args;
96
- let inner = GlobalComponentMemories.map.get(componentIdx);
97
- if (!inner) {
98
- inner = [];
99
- GlobalComponentMemories.map.set(componentIdx, inner);
100
- }
101
- inner.push({ memory, idx });
129
+ if (args.memoryIdx === undefined) {
130
+ return Object.values(metas);
102
131
  }
103
132
 
104
- static getMemoriesForComponentIdx(componentIdx) {
105
- const metas = GlobalComponentMemories.map.get(componentIdx);
106
- return metas.map(meta => meta.memory);
133
+ const meta = metas[args.memoryIdx];
134
+ return meta?.memory;
135
+ }
136
+
137
+ function registerGlobalMemoryForComponent(args) {
138
+ const { componentIdx, memory, memoryIdx } = args ?? {};
139
+ if (componentIdx === undefined) { throw new TypeError('missing component idx'); }
140
+ if (memory === undefined && memoryIdx === undefined) { throw new TypeError('missing both memory & memory idx'); }
141
+ let inner = GLOBAL_COMPONENT_MEMORY_MAP.get(componentIdx);
142
+ if (!inner) {
143
+ inner = {};
144
+ GLOBAL_COMPONENT_MEMORY_MAP.set(componentIdx, inner);
107
145
  }
108
146
 
109
- static getMemory(componentIdx, idx) {
110
- const metas = GlobalComponentMemories.map.get(componentIdx);
111
- return metas.find(meta => meta.idx === idx)?.memory;
112
- }
147
+ inner[memoryIdx] = { memory, memoryIdx, componentIdx };
113
148
  }
114
149
 
115
150
  class RepTable {
@@ -120,23 +155,30 @@ class RepTable {
120
155
  this.target = args?.target;
121
156
  }
122
157
 
158
+ data() { return this.#data; }
159
+
123
160
  insert(val) {
124
161
  _debugLog('[RepTable#insert()] args', { val, target: this.target });
125
162
  const freeIdx = this.#data[0];
126
163
  if (freeIdx === 0) {
127
164
  this.#data.push(val);
128
165
  this.#data.push(null);
129
- return (this.#data.length >> 1) - 1;
166
+ const rep = (this.#data.length >> 1) - 1;
167
+ _debugLog('[RepTable#insert()] inserted', { val, target: this.target, rep });
168
+ return rep;
130
169
  }
131
170
  this.#data[0] = this.#data[freeIdx << 1];
132
171
  const placementIdx = freeIdx << 1;
133
172
  this.#data[placementIdx] = val;
134
173
  this.#data[placementIdx + 1] = null;
174
+ _debugLog('[RepTable#insert()] inserted', { val, target: this.target, rep: freeIdx });
135
175
  return freeIdx;
136
176
  }
137
177
 
138
178
  get(rep) {
139
179
  _debugLog('[RepTable#get()] args', { rep, target: this.target });
180
+ if (rep === 0) { throw new Error('invalid resource rep during get, (cannot be 0)'); }
181
+
140
182
  const baseIdx = rep << 1;
141
183
  const val = this.#data[baseIdx];
142
184
  return val;
@@ -144,17 +186,19 @@ class RepTable {
144
186
 
145
187
  contains(rep) {
146
188
  _debugLog('[RepTable#contains()] args', { rep, target: this.target });
189
+ if (rep === 0) { throw new Error('invalid resource rep during contains, (cannot be 0)'); }
190
+
147
191
  const baseIdx = rep << 1;
148
192
  return !!this.#data[baseIdx];
149
193
  }
150
194
 
151
195
  remove(rep) {
152
196
  _debugLog('[RepTable#remove()] args', { rep, target: this.target });
197
+ if (rep === 0) { throw new Error('invalid resource rep during remove, (cannot be 0)'); }
153
198
  if (this.#data.length === 2) { throw new Error('invalid'); }
154
199
 
155
200
  const baseIdx = rep << 1;
156
201
  const val = this.#data[baseIdx];
157
- if (val === 0) { throw new Error('invalid resource rep (cannot be 0)'); }
158
202
 
159
203
  this.#data[baseIdx] = this.#data[0];
160
204
  this.#data[0] = rep;
@@ -178,11 +222,41 @@ const _typeCheckAsyncFn= (f) => {
178
222
  };
179
223
 
180
224
  const ASYNC_FN_CTOR = (async () => {}).constructor;
225
+
226
+ function clearCurrentTask(componentIdx, taskID) {
227
+ _debugLog('[clearCurrentTask()] args', { componentIdx, taskID });
228
+
229
+ if (componentIdx === undefined || componentIdx === null) {
230
+ throw new Error('missing/invalid component instance index while ending current task');
231
+ }
232
+
233
+ const tasks = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
234
+ if (!tasks || !Array.isArray(tasks)) {
235
+ throw new Error('missing/invalid tasks for component instance while ending task');
236
+ }
237
+ if (tasks.length == 0) {
238
+ throw new Error(`no current tasks for component instance [${componentIdx}] while ending task`);
239
+ }
240
+
241
+ if (taskID !== undefined) {
242
+ const last = tasks[tasks.length - 1];
243
+ if (last.id !== taskID) {
244
+ // throw new Error('current task does not match expected task ID');
245
+ return;
246
+ }
247
+ }
248
+
249
+ ASYNC_CURRENT_TASK_IDS.pop();
250
+ ASYNC_CURRENT_COMPONENT_IDXS.pop();
251
+
252
+ const taskMeta = tasks.pop();
253
+ return taskMeta.task;
254
+ }
255
+ const CURRENT_TASK_MAY_BLOCK = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
181
256
  const ASYNC_CURRENT_TASK_IDS = [];
182
257
  const ASYNC_CURRENT_COMPONENT_IDXS = [];
183
258
 
184
259
  function unpackCallbackResult(result) {
185
- _debugLog('[unpackCallbackResult()] args', { result });
186
260
  if (!(_typeCheckValidI32(result))) { throw new Error('invalid callback return value [' + result + '], not a valid i32'); }
187
261
  const eventCode = result & 0xF;
188
262
  if (eventCode < 0 || eventCode > 3) {
@@ -194,17 +268,345 @@ function unpackCallbackResult(result) {
194
268
  return [eventCode, waitableSetRep];
195
269
  }
196
270
 
197
- function promiseWithResolvers() {
198
- if (Promise.withResolvers) {
199
- return Promise.withResolvers();
200
- } else {
201
- let resolve;
202
- let reject;
203
- const promise = new Promise((res, rej) => {
204
- resolve = res;
205
- reject = rej;
271
+ class AsyncSubtask {
272
+ static _ID = 0n;
273
+
274
+ static State = {
275
+ STARTING: 0,
276
+ STARTED: 1,
277
+ RETURNED: 2,
278
+ CANCELLED_BEFORE_STARTED: 3,
279
+ CANCELLED_BEFORE_RETURNED: 4,
280
+ };
281
+
282
+ #id;
283
+ #state = AsyncSubtask.State.STARTING;
284
+ #componentIdx;
285
+
286
+ #parentTask;
287
+ #childTask = null;
288
+
289
+ #dropped = false;
290
+ #cancelRequested = false;
291
+
292
+ #memoryIdx = null;
293
+ #lenders = null;
294
+
295
+ #waitable = null;
296
+
297
+ #callbackFn = null;
298
+ #callbackFnName = null;
299
+
300
+ #postReturnFn = null;
301
+ #onProgressFn = null;
302
+ #pendingEventFn = null;
303
+
304
+ #callMetadata = {};
305
+
306
+ #resolved = false;
307
+
308
+ #onResolveHandlers = [];
309
+ #onStartHandlers = [];
310
+
311
+ #result = null;
312
+ #resultSet = false;
313
+
314
+ fnName;
315
+ target;
316
+ isAsync;
317
+ isManualAsync;
318
+
319
+ constructor(args) {
320
+ if (typeof args.componentIdx !== 'number') {
321
+ throw new Error('invalid componentIdx for subtask creation');
322
+ }
323
+ this.#componentIdx = args.componentIdx;
324
+
325
+ this.#id = ++AsyncSubtask._ID;
326
+ this.fnName = args.fnName;
327
+
328
+ if (!args.parentTask) { throw new Error('missing parent task during subtask creation'); }
329
+ this.#parentTask = args.parentTask;
330
+
331
+ if (args.childTask) { this.#childTask = args.childTask; }
332
+
333
+ if (args.memoryIdx) { this.#memoryIdx = args.memoryIdx; }
334
+
335
+ if (!args.waitable) { throw new Error("missing/invalid waitable"); }
336
+ this.#waitable = args.waitable;
337
+
338
+ if (args.callMetadata) { this.#callMetadata = args.callMetadata; }
339
+
340
+ this.#lenders = [];
341
+ this.target = args.target;
342
+ this.isAsync = args.isAsync;
343
+ this.isManualAsync = args.isManualAsync;
344
+ }
345
+
346
+ id() { return this.#id; }
347
+ parentTaskID() { return this.#parentTask?.id(); }
348
+ childTaskID() { return this.#childTask?.id(); }
349
+ state() { return this.#state; }
350
+
351
+ waitable() { return this.#waitable; }
352
+ waitableRep() { return this.#waitable.idx(); }
353
+
354
+ join() { return this.#waitable.join(...arguments); }
355
+ getPendingEvent() { return this.#waitable.getPendingEvent(...arguments); }
356
+ hasPendingEvent() { return this.#waitable.hasPendingEvent(...arguments); }
357
+ setPendingEvent() { return this.#waitable.setPendingEvent(...arguments); }
358
+
359
+ setTarget(tgt) { this.target = tgt; }
360
+
361
+ getResult() {
362
+ if (!this.#resultSet) { throw new Error("subtask result has not been set") }
363
+ return this.#result;
364
+ }
365
+ setResult(v) {
366
+ if (this.#resultSet) { throw new Error("subtask result has already been set"); }
367
+ this.#result = v;
368
+ this.#resultSet = true;
369
+ }
370
+
371
+ componentIdx() { return this.#componentIdx; }
372
+
373
+ setChildTask(t) {
374
+ if (!t) { throw new Error('cannot set missing/invalid child task on subtask'); }
375
+ if (this.#childTask) { throw new Error('child task is already set on subtask'); }
376
+ if (this.#parentTask === t) { throw new Error("parent cannot be child"); }
377
+ this.#childTask = t;
378
+ }
379
+ getChildTask(t) { return this.#childTask; }
380
+
381
+ getParentTask() { return this.#parentTask; }
382
+
383
+ setCallbackFn(f, name) {
384
+ if (!f) { return; }
385
+ if (this.#callbackFn) { throw new Error('callback fn can only be set once'); }
386
+ this.#callbackFn = f;
387
+ this.#callbackFnName = name;
388
+ }
389
+
390
+ getCallbackFnName() {
391
+ if (!this.#callbackFn) { return undefined; }
392
+ return this.#callbackFn.name;
393
+ }
394
+
395
+ setPostReturnFn(f) {
396
+ if (!f) { return; }
397
+ if (this.#postReturnFn) { throw new Error('postReturn fn can only be set once'); }
398
+ this.#postReturnFn = f;
399
+ }
400
+
401
+ setOnProgressFn(f) {
402
+ if (this.#onProgressFn) { throw new Error('on progress fn can only be set once'); }
403
+ this.#onProgressFn = f;
404
+ }
405
+
406
+ isNotStarted() {
407
+ return this.#state == AsyncSubtask.State.STARTING;
408
+ }
409
+
410
+ registerOnStartHandler(f) {
411
+ this.#onStartHandlers.push(f);
412
+ }
413
+
414
+ onStart(args) {
415
+ _debugLog('[AsyncSubtask#onStart()] args', {
416
+ componentIdx: this.#componentIdx,
417
+ subtaskID: this.#id,
418
+ parentTaskID: this.parentTaskID(),
419
+ fnName: this.fnName,
206
420
  });
207
- return { promise, resolve, reject };
421
+
422
+ if (this.#onProgressFn) { this.#onProgressFn(); }
423
+
424
+ this.#state = AsyncSubtask.State.STARTED;
425
+
426
+ let result;
427
+
428
+ // If we have been provided a helper start function as a result of
429
+ // component fusion performed by wasmtime tooling, then we can call that helper and lifts/lowers will
430
+ // be performed for us.
431
+ //
432
+ // See also documentation on `HostIntrinsic::PrepareCall`
433
+ //
434
+ if (this.#callMetadata.startFn) {
435
+ result = this.#callMetadata.startFn.apply(null, args?.startFnParams ?? []);
436
+ }
437
+
438
+ return result;
439
+ }
440
+
441
+
442
+ registerOnResolveHandler(f) {
443
+ this.#onResolveHandlers.push(f);
444
+ }
445
+
446
+ reject(subtaskErr) {
447
+ this.#childTask?.reject(subtaskErr);
448
+ }
449
+
450
+ onResolve(subtaskValue) {
451
+ _debugLog('[AsyncSubtask#onResolve()] args', {
452
+ componentIdx: this.#componentIdx,
453
+ subtaskID: this.#id,
454
+ isAsync: this.isAsync,
455
+ childTaskID: this.childTaskID(),
456
+ parentTaskID: this.parentTaskID(),
457
+ parentTaskFnName: this.#parentTask?.entryFnName(),
458
+ fnName: this.fnName,
459
+ });
460
+
461
+ if (this.#resolved) {
462
+ throw new Error('subtask has already been resolved');
463
+ }
464
+
465
+ if (this.#onProgressFn) { this.#onProgressFn(); }
466
+
467
+ if (subtaskValue === null) {
468
+ if (this.#cancelRequested) {
469
+ throw new Error('cancel was not requested, but no value present at return');
470
+ }
471
+
472
+ if (this.#state === AsyncSubtask.State.STARTING) {
473
+ this.#state = AsyncSubtask.State.CANCELLED_BEFORE_STARTED;
474
+ } else {
475
+ if (this.#state !== AsyncSubtask.State.STARTED) {
476
+ throw new Error('resolved subtask must have been started before cancellation');
477
+ }
478
+ this.#state = AsyncSubtask.State.CANCELLED_BEFORE_RETURNED;
479
+ }
480
+ } else {
481
+ if (this.#state !== AsyncSubtask.State.STARTED) {
482
+ throw new Error('resolved subtask must have been started before completion');
483
+ }
484
+ this.#state = AsyncSubtask.State.RETURNED;
485
+ }
486
+
487
+ this.setResult(subtaskValue);
488
+
489
+ for (const f of this.#onResolveHandlers) {
490
+ try {
491
+ f(subtaskValue);
492
+ } catch (err) {
493
+ console.error("error during subtask resolve handler", err);
494
+ throw err;
495
+ }
496
+ }
497
+
498
+ const callMetadata = this.getCallMetadata();
499
+
500
+ // TODO(fix): we should be able to easily have the caller's meomry
501
+ // to lower into here, but it's not present in PrepareCall
502
+ const memory = callMetadata.memory ?? this.#parentTask?.getReturnMemory() ?? lookupMemoriesForComponent({ componentIdx: this.#parentTask?.componentIdx() })[0];
503
+ if (callMetadata && !callMetadata.returnFn && this.isAsync && callMetadata.resultPtr && memory) {
504
+ const { resultPtr, realloc } = callMetadata;
505
+ const lowers = callMetadata.lowers; // may have been updated in task.return of the child
506
+ if (lowers && lowers.length > 0) {
507
+ lowers[0]({
508
+ componentIdx: this.#componentIdx,
509
+ memory,
510
+ realloc,
511
+ vals: [subtaskValue],
512
+ storagePtr: resultPtr,
513
+ });
514
+ }
515
+ }
516
+
517
+ this.#resolved = true;
518
+ this.#parentTask.removeSubtask(this);
519
+ }
520
+
521
+ getStateNumber() { return this.#state; }
522
+ isReturned() { return this.#state === AsyncSubtask.State.RETURNED; }
523
+
524
+ getCallMetadata() { return this.#callMetadata; }
525
+
526
+ isResolved() {
527
+ if (this.#state === AsyncSubtask.State.STARTING
528
+ || this.#state === AsyncSubtask.State.STARTED) {
529
+ return false;
530
+ }
531
+ if (this.#state === AsyncSubtask.State.RETURNED
532
+ || this.#state === AsyncSubtask.State.CANCELLED_BEFORE_STARTED
533
+ || this.#state === AsyncSubtask.State.CANCELLED_BEFORE_RETURNED) {
534
+ return true;
535
+ }
536
+ throw new Error('unrecognized internal Subtask state [' + this.#state + ']');
537
+ }
538
+
539
+ addLender(handle) {
540
+ _debugLog('[AsyncSubtask#addLender()] args', { handle });
541
+ if (!Number.isNumber(handle)) { throw new Error('missing/invalid lender handle [' + handle + ']'); }
542
+
543
+ if (this.#lenders.length === 0 || this.isResolved()) {
544
+ throw new Error('subtask has no lendors or has already been resolved');
545
+ }
546
+
547
+ handle.lends++;
548
+ this.#lenders.push(handle);
549
+ }
550
+
551
+ deliverResolve() {
552
+ _debugLog('[AsyncSubtask#deliverResolve()] args', {
553
+ lenders: this.#lenders,
554
+ parentTaskID: this.parentTaskID(),
555
+ subtaskID: this.#id,
556
+ childTaskID: this.childTaskID(),
557
+ resolved: this.isResolved(),
558
+ resolveDelivered: this.resolveDelivered(),
559
+ });
560
+
561
+ const cannotDeliverResolve = this.resolveDelivered() || !this.isResolved();
562
+ if (cannotDeliverResolve) {
563
+ throw new Error('subtask cannot deliver resolution twice, and the subtask must be resolved');
564
+ }
565
+
566
+ for (const lender of this.#lenders) {
567
+ lender.lends--;
568
+ }
569
+
570
+ this.#lenders = null;
571
+ }
572
+
573
+ resolveDelivered() {
574
+ _debugLog('[AsyncSubtask#resolveDelivered()] args', { });
575
+ if (this.#lenders === null && !this.isResolved()) {
576
+ throw new Error('invalid subtask state, lenders missing and subtask has not been resolved');
577
+ }
578
+ return this.#lenders === null;
579
+ }
580
+
581
+ drop() {
582
+ _debugLog('[AsyncSubtask#drop()] args', {
583
+ componentIdx: this.#componentIdx,
584
+ parentTaskID: this.#parentTask?.id(),
585
+ parentTaskFnName: this.#parentTask?.entryFnName(),
586
+ childTaskID: this.#childTask?.id(),
587
+ childTaskFnName: this.#childTask?.entryFnName(),
588
+ subtaskFnName: this.fnName,
589
+ });
590
+ if (!this.#waitable) { throw new Error('missing/invalid inner waitable'); }
591
+ if (!this.resolveDelivered()) {
592
+ throw new Error('cannot drop subtask before resolve is delivered');
593
+ }
594
+ if (this.#waitable) { this.#waitable.drop() }
595
+ this.#dropped = true;
596
+ }
597
+
598
+ #getComponentState() {
599
+ const state = getOrCreateAsyncState(this.#componentIdx);
600
+ if (!state) {
601
+ throw new Error('invalid/missing async state for component [' + componentIdx + ']');
602
+ }
603
+ return state;
604
+ }
605
+
606
+ getWaitableHandleIdx() {
607
+ _debugLog('[AsyncSubtask#getWaitableHandleIdx()] args', { });
608
+ if (!this.#waitable) { throw new Error('missing/invalid waitable'); }
609
+ return this.waitableRep();
208
610
  }
209
611
  }
210
612
 
@@ -213,46 +615,40 @@ memoryIdx,
213
615
  getMemoryFn,
214
616
  startFn,
215
617
  returnFn,
216
- callerInstanceIdx,
217
- calleeInstanceIdx,
618
+ callerComponentIdx,
619
+ calleeComponentIdx,
218
620
  taskReturnTypeIdx,
219
- isCalleeAsyncInt,
621
+ calleeIsAsyncInt,
220
622
  stringEncoding,
221
623
  resultCountOrAsync,
222
624
  ) {
223
625
  _debugLog('[_prepareCall()]', {
224
- callerInstanceIdx,
225
- calleeInstanceIdx,
626
+ memoryIdx,
627
+ callerComponentIdx,
628
+ calleeComponentIdx,
226
629
  taskReturnTypeIdx,
227
- isCalleeAsyncInt,
630
+ calleeIsAsyncInt,
228
631
  stringEncoding,
229
632
  resultCountOrAsync,
230
633
  });
231
634
  const argArray = [...arguments];
232
635
 
233
- // Since Rust will happily pass large u32s over, resultCountOrAsync should be one of:
234
- // (a) u32 max size => callee is async fn with no result
235
- // (b) u32 max size - 1 => callee is async fn with result
236
- // (c) any other value => callee is sync with the given result count
237
- //
238
- // Due to JS handling the value as 2s complement, the `resultCountOrAsync` ends up being:
239
- // (a) -1 as u32 max size
240
- // (b) -2 as u32 max size - 1
241
- // (c) x
242
- //
243
- // Due to JS mishandling the value as 2s complement, the actual values we get are:
244
- // see. https://github.com/wasm-bindgen/wasm-bindgen/issues/1388
636
+ // value passed in *may* be as large as u32::MAX which may be mangled into -2
637
+ resultCountOrAsync >>>= 0;
638
+
245
639
  let isAsync = false;
246
640
  let hasResultPointer = false;
247
- if (resultCountOrAsync === -1) {
641
+ if (resultCountOrAsync === 2**32 - 1) {
642
+ // prepare async with no result (u32::MAX)
248
643
  isAsync = true;
249
644
  hasResultPointer = false;
250
- } else if (resultCountOrAsync === -2) {
645
+ } else if (resultCountOrAsync === 2**32 - 2) {
646
+ // prepare async with result (u32::MAX - 1)
251
647
  isAsync = true;
252
648
  hasResultPointer = true;
253
649
  }
254
650
 
255
- const currentCallerTaskMeta = getCurrentTask(callerInstanceIdx);
651
+ const currentCallerTaskMeta = getCurrentTask(callerComponentIdx);
256
652
  if (!currentCallerTaskMeta) {
257
653
  throw new Error('invalid/missing current task for caller during prepare call');
258
654
  }
@@ -262,18 +658,19 @@ resultCountOrAsync,
262
658
  throw new Error('unexpectedly missing task in meta for caller during prepare call');
263
659
  }
264
660
 
265
- if (currentCallerTask.componentIdx() !== callerInstanceIdx) {
266
- throw new Error(`task component idx [${ currentCallerTask.componentIdx() }] !== [${ callerInstanceIdx }] (callee ${ calleeInstanceIdx })`);
661
+ if (currentCallerTask.componentIdx() !== callerComponentIdx) {
662
+ throw new Error(`task component idx [${ currentCallerTask.componentIdx() }] !== [${ callerComponentIdx }] (callee ${ calleeComponentIdx })`);
267
663
  }
268
664
 
269
665
  let getCalleeParamsFn;
270
666
  let resultPtr = null;
667
+ let directParamsArr;
271
668
  if (hasResultPointer) {
272
- const directParamsArr = argArray.slice(11);
669
+ directParamsArr = argArray.slice(10, argArray.length - 1);
273
670
  getCalleeParamsFn = () => directParamsArr;
274
- resultPtr = argArray[10];
671
+ resultPtr = argArray[argArray.length - 1];
275
672
  } else {
276
- const directParamsArr = argArray.slice(10);
673
+ directParamsArr = argArray.slice(10);
277
674
  getCalleeParamsFn = () => directParamsArr;
278
675
  }
279
676
 
@@ -292,21 +689,12 @@ resultCountOrAsync,
292
689
  throw new Error(`unrecognized string encoding enum [${stringEncoding}]`);
293
690
  }
294
691
 
295
- const [newTask, newTaskID] = createNewCurrentTask({
296
- componentIdx: calleeInstanceIdx,
297
- isAsync: isCalleeAsyncInt !== 0,
298
- getCalleeParamsFn,
299
- // TODO: find a way to pass the import name through here
300
- entryFnName: 'task/' + currentCallerTask.id() + '/new-prepare-task',
301
- stringEncoding,
302
- });
303
-
304
692
  const subtask = currentCallerTask.createSubtask({
305
- componentIdx: callerInstanceIdx,
693
+ componentIdx: callerComponentIdx,
306
694
  parentTask: currentCallerTask,
307
- childTask: newTask,
695
+ isAsync,
308
696
  callMetadata: {
309
- memory: getMemoryFn(),
697
+ getMemoryFn,
310
698
  memoryIdx,
311
699
  resultPtr,
312
700
  returnFn,
@@ -314,26 +702,75 @@ resultCountOrAsync,
314
702
  }
315
703
  });
316
704
 
705
+ const [newTask, newTaskID] = createNewCurrentTask({
706
+ componentIdx: calleeComponentIdx,
707
+ isAsync,
708
+ getCalleeParamsFn,
709
+ entryFnName: [
710
+ 'task',
711
+ subtask.getParentTask().id(),
712
+ 'subtask',
713
+ subtask.id(),
714
+ 'new-prepared-async-task'
715
+ ].join('/'),
716
+ stringEncoding,
717
+ });
317
718
  newTask.setParentSubtask(subtask);
318
- // NOTE: This isn't really a return memory idx for the caller, it's for checking
319
- // against the task.return (which will be called from the callee)
320
719
  newTask.setReturnMemoryIdx(memoryIdx);
720
+ newTask.setReturnMemory(getMemoryFn);
721
+ subtask.setChildTask(newTask);
722
+
723
+ newTask.subtaskMeta = {
724
+ subtask,
725
+ calleeComponentIdx,
726
+ callerComponentIdx,
727
+ getCalleeParamsFn,
728
+ stringEncoding,
729
+ isAsync,
730
+ };
731
+
732
+ _setGlobalCurrentTaskMeta({
733
+ taskID: newTask.id(),
734
+ componentIdx: newTask.componentIdx(),
735
+ });
321
736
  }
322
737
 
323
738
  function _asyncStartCall(args, callee, paramCount, resultCount, flags) {
324
- const { getCallbackFn, callbackIdx, getPostReturnFn, postReturnIdx } = args;
325
- _debugLog('[_asyncStartCall()] args', args);
739
+ const componentIdx = ASYNC_CURRENT_COMPONENT_IDXS.at(-1);
326
740
 
327
- const taskMeta = getCurrentTask(ASYNC_CURRENT_COMPONENT_IDXS.at(-1), ASYNC_CURRENT_TASK_IDS.at(-1));
328
- if (!taskMeta) { throw new Error('invalid/missing current async task meta during prepare call'); }
741
+ const globalTaskMeta = _getGlobalCurrentTaskMeta(componentIdx);
742
+ if (!globalTaskMeta) { throw new Error('missing global current task globalTaskMeta'); }
743
+ const taskID = globalTaskMeta.taskID;
329
744
 
330
- const argArray = [...arguments];
745
+ _debugLog('[_asyncStartCall()] args', { args, componentIdx });
746
+ const { getCallbackFn, callbackIdx, getPostReturnFn, postReturnIdx } = args;
331
747
 
332
- // NOTE: at this point we know the current task is the one that was started
333
- // in PrepareCall, so we *should* be able to pop it back off and be left with
334
- // the previous task
335
- const preparedTask = taskMeta.task;
336
- if (!preparedTask) { throw new Error('unexpectedly missing task in task meta during prepare call'); }
748
+ const preparedTaskMeta = getCurrentTask(componentIdx, taskID);
749
+ if (!preparedTaskMeta) { throw new Error('unexpectedly missing current task'); }
750
+
751
+ const preparedTask = preparedTaskMeta.task;
752
+ if (!preparedTask) { throw new Error('unexpectedly missing current task'); }
753
+ if (!preparedTask.subtaskMeta) { throw new Error('missing subtask meta from prepare'); }
754
+
755
+ const {
756
+ subtask,
757
+ returnMemoryIdx,
758
+ getReturnMemoryFn,
759
+ callerComponentIdx,
760
+ calleeComponentIdx,
761
+ getCalleeParamsFn,
762
+ isAsync,
763
+ stringEncoding,
764
+ } = preparedTask.subtaskMeta;
765
+ if (!subtask) { throw new Error("missing subtask from cstate during async start call"); }
766
+ if (calleeComponentIdx !== preparedTask.componentIdx()) {
767
+ throw new Error(`meta callee idx [${calleeComponentIdx}] != current task idx [${preparedTask.componentIdx()}] during async start call`);
768
+ }
769
+ if (calleeComponentIdx !== componentIdx) {
770
+ throw new Error("mismatched componentIdx for async start call (does not match prepare)");
771
+ }
772
+
773
+ const argArray = [...arguments];
337
774
 
338
775
  if (resultCount < 0 || resultCount > 1) { throw new Error('invalid/unsupported result count'); }
339
776
 
@@ -342,34 +779,16 @@ function _asyncStartCall(args, callee, paramCount, resultCount, flags) {
342
779
  preparedTask.setCallbackFn(callbackFn, callbackFnName);
343
780
  preparedTask.setPostReturnFn(getPostReturnFn());
344
781
 
345
- const subtask = preparedTask.getParentSubtask();
346
-
347
- if (resultCount < 0 || resultCount > 1) { throw new Error(`unsupported result count [${ resultCount }]`); }
782
+ if (resultCount < 0 || resultCount > 1) {
783
+ throw new Error(`unsupported result count [${ resultCount }]`);
784
+ }
348
785
 
349
786
  const params = preparedTask.getCalleeParams();
350
787
  if (paramCount !== params.length) {
351
788
  throw new Error(`unexpected callee param count [${ params.length }], _asyncStartCall invocation expected [${ paramCount }]`);
352
789
  }
353
790
 
354
- subtask.setOnProgressFn(() => {
355
- subtask.setPendingEventFn(() => {
356
- if (subtask.resolved()) { subtask.deliverResolve(); }
357
- return {
358
- code: ASYNC_EVENT_CODE.SUBTASK,
359
- index: rep,
360
- result: subtask.getStateNumber(),
361
- }
362
- });
363
- });
364
-
365
- const subtaskState = subtask.getStateNumber();
366
- if (subtaskState < 0 || subtaskState > 2**5) {
367
- throw new Error('invalid subtask state, out of valid range');
368
- }
369
-
370
791
  const callerComponentState = getOrCreateAsyncState(subtask.componentIdx());
371
- const rep = callerComponentState.subtasks.insert(subtask);
372
- subtask.setRep(rep);
373
792
 
374
793
  const calleeComponentState = getOrCreateAsyncState(preparedTask.componentIdx());
375
794
  const calleeBackpressure = calleeComponentState.hasBackpressure();
@@ -380,6 +799,7 @@ function _asyncStartCall(args, callee, paramCount, resultCount, flags) {
380
799
  // lowering manually, as fused modules provider helper functions that can
381
800
  subtask.registerOnResolveHandler((res) => {
382
801
  _debugLog('[_asyncStartCall()] handling subtask result', { res, subtaskID: subtask.id() });
802
+
383
803
  let subtaskCallMeta = subtask.getCallMetadata();
384
804
 
385
805
  // NOTE: in the case of guest -> guest async calls, there may be no memory/realloc present,
@@ -397,8 +817,13 @@ function _asyncStartCall(args, callee, paramCount, resultCount, flags) {
397
817
 
398
818
  // If a helper function was provided we are likely in a fused guest->guest call,
399
819
  // and the result will be delivered (lift/lowered) via helper function
400
- if (subtaskCallMeta.returnFn) {
401
- _debugLog('[_asyncStartCall()] return function present while ahndling subtask result, returning early (skipping lower)');
820
+ if (subtaskCallMeta && subtaskCallMeta.returnFn) {
821
+ _debugLog('[_asyncStartCall()] return function present while handling subtask result, returning early (skipping lower)');
822
+
823
+ // TODO: centralize calling of returnFn to *one place* (if possible)
824
+ if (subtaskCallMeta.returnFnCalled) { return; }
825
+
826
+ subtaskCallMeta.returnFn.apply(null, [subtaskCallMeta.resultPtr]);
402
827
  return;
403
828
  }
404
829
 
@@ -409,27 +834,31 @@ function _asyncStartCall(args, callee, paramCount, resultCount, flags) {
409
834
  }
410
835
 
411
836
  let callerMemory;
412
- if (callerMemoryIdx) {
413
- callerMemory = GlobalComponentMemories.getMemory(callerComponentIdx, callerMemoryIdx);
837
+ if (callerMemoryIdx !== null && callerMemoryIdx !== undefined) {
838
+ callerMemory = lookupMemoriesForComponent({ componentIdx: callerComponentIdx, memoryIdx: callerMemoryIdx });
414
839
  } else {
415
- const callerMemories = GlobalComponentMemories.getMemoriesForComponentIdx(callerComponentIdx);
416
- if (callerMemories.length != 1) { throw new Error(`unsupported amount of caller memories`); }
840
+ const callerMemories = lookupMemoriesForComponent({ componentIdx: callerComponentIdx });
841
+ if (callerMemories.length !== 1) { throw new Error(`unsupported amount of caller memories`); }
417
842
  callerMemory = callerMemories[0];
418
843
  }
419
844
 
420
845
  if (!callerMemory) {
846
+ _debugLog('[_asyncStartCall()] missing memory', { subtaskID: subtask.id(), res });
421
847
  throw new Error(`missing memory for to guest->guest call result (subtask [${subtask.id()}])`);
422
848
  }
423
849
 
424
850
  const lowerFns = calleeTask.getReturnLowerFns();
425
851
  if (!lowerFns || lowerFns.length === 0) {
426
- throw new Error(`missing result lower metadata for guest->guests call (subtask [${subtask.id()}])`);
852
+ _debugLog('[_asyncStartCall()] missing result lower metadata for guest->guest call', { subtaskID: subtask.id() });
853
+ throw new Error(`missing result lower metadata for guest->guest call (subtask [${subtask.id()}])`);
427
854
  }
428
855
 
429
856
  if (lowerFns.length !== 1) {
857
+ _debugLog('[_asyncStartCall()] only single result reportetd for guest->guest call', { subtaskID: subtask.id() });
430
858
  throw new Error(`only single result supported for guest->guest calls (subtask [${subtask.id()}])`);
431
859
  }
432
860
 
861
+ _debugLog('[_asyncStartCall()] lowering results', { subtaskID: subtask.id() });
433
862
  lowerFns[0]({
434
863
  realloc: undefined,
435
864
  memory: callerMemory,
@@ -440,115 +869,96 @@ function _asyncStartCall(args, callee, paramCount, resultCount, flags) {
440
869
 
441
870
  });
442
871
 
443
- // Build call params
444
- const subtaskCallMeta = subtask.getCallMetadata();
445
- let startFnParams = [];
446
- let calleeParams = [];
447
- if (subtaskCallMeta.startFn && subtaskCallMeta.resultPtr) {
448
- // If we're using a fused component start fn and a result pointer is present,
449
- // then we need to pass the result pointer and other params to the start fn
450
- startFnParams.push(subtaskCallMeta.resultPtr, ...params);
451
- } else {
452
- // if not we need to pass params to the callee instead
453
- startFnParams.push(...params);
454
- calleeParams.push(...params);
455
- }
456
-
457
- preparedTask.registerOnResolveHandler((res) => {
458
- _debugLog('[_asyncStartCall()] signaling subtask completion due to task completion', {
459
- childTaskID: preparedTask.id(),
460
- subtaskID: subtask.id(),
461
- parentTaskID: subtask.getParentTask().id(),
872
+ subtask.setOnProgressFn(() => {
873
+ subtask.setPendingEvent(() => {
874
+ if (subtask.isResolved()) { subtask.deliverResolve(); }
875
+ const event = {
876
+ code: ASYNC_EVENT_CODE.SUBTASK,
877
+ payload0: subtask.waitableRep(),
878
+ payload1: subtask.getStateNumber(),
879
+ };
880
+ return event;
462
881
  });
463
- subtask.onResolve(res);
464
- });
465
-
466
- // TODO(fix): start fns sometimes produce results, how should they be used?
467
- // the result should theoretically be used for flat lowering, but fused components do
468
- // this automatically!
469
- subtask.onStart({ startFnParams });
470
-
471
- _debugLog("[_asyncStartCall()] initial call", {
472
- task: preparedTask.id(),
473
- subtaskID: subtask.id(),
474
- calleeFnName: callee.name,
475
- });
476
-
477
- const callbackResult = callee.apply(null, calleeParams);
478
-
479
- _debugLog("[_asyncStartCall()] after initial call", {
480
- task: preparedTask.id(),
481
- subtaskID: subtask.id(),
482
- calleeFnName: callee.name,
483
882
  });
484
883
 
485
- const doSubtaskResolve = () => {
486
- subtask.deliverResolve();
487
- };
488
-
489
- // If a single call resolved the subtask and there is no backpressure in the guest,
490
- // we can return immediately
491
- if (subtask.resolved() && !calleeBackpressure) {
492
- _debugLog("[_asyncStartCall()] instantly resolved", {
493
- calleeComponentIdx: preparedTask.componentIdx(),
494
- task: preparedTask.id(),
495
- subtaskID: subtask.id(),
496
- callerComponentIdx: subtask.componentIdx(),
884
+ // Start the (event) driver loop that will resolve the task
885
+ queueMicrotask(async () => {
886
+ let startRes = subtask.onStart({ startFnParams: params });
887
+ startRes = Array.isArray(startRes) ? startRes : [startRes];
888
+
889
+ await calleeComponentState.suspendTask({
890
+ task: preparedTask,
891
+ readyFn: () => !calleeComponentState.isExclusivelyLocked(),
497
892
  });
498
893
 
499
- // If a fused component return function was specified for the subtask,
500
- // we've likely already called it during resolution of the task.
501
- //
502
- // In this case, we do not want to actually return 2 AKA "RETURNED",
503
- // but the normal started task state, because the fused component expects to get
504
- // the waitable + the original subtask state (0 AKA "STARTING")
505
- //
506
- if (subtask.getCallMetadata().returnFn) {
507
- return Number(subtask.waitableRep()) << 4 | subtaskState;
894
+ const started = await preparedTask.enter();
895
+ if (!started) {
896
+ _debugLog('[_asyncStartCall()] task failed early', {
897
+ taskID: preparedTask.id(),
898
+ subtaskID: subtask.id(),
899
+ });
900
+ throw new Error("task failed to start");
901
+ return;
508
902
  }
509
903
 
510
- doSubtaskResolve();
511
- return AsyncSubtask.State.RETURNED;
512
- }
513
-
514
- // Start the (event) driver loop that will resolve the task
515
- new Promise(async (resolve, reject) => {
516
- if (subtask.resolved() && calleeBackpressure) {
517
- await calleeComponentState.waitForBackpressure();
518
-
519
- _debugLog("[_asyncStartCall()] instantly resolved after cleared backpressure", {
520
- calleeComponentIdx: preparedTask.componentIdx(),
521
- task: preparedTask.id(),
522
- subtaskID: subtask.id(),
523
- callerComponentIdx: subtask.componentIdx(),
904
+ let callbackResult;
905
+ try {
906
+ let jspiCallee = WebAssembly.promising(callee);
907
+ callbackResult = await _withGlobalCurrentTaskMetaAsync({
908
+ taskID: preparedTask.id(),
909
+ componentIdx: preparedTask.componentIdx(),
910
+ fn: () => {
911
+ return jspiCallee.apply(null, startRes);
912
+ }
524
913
  });
914
+ } catch(err) {
915
+ _debugLog("[_asyncStartCall()] initial subtask callee run failed", err);
916
+ // NOTE: a good place to rejectt the parent task, if rejection API is enabled
917
+ // subtask.reject(err);
918
+ // subtask.getParentTask().reject(err);
919
+
920
+ subtask.getParentTask().setErrored(err);
921
+
525
922
  return;
526
923
  }
527
924
 
528
- const started = await preparedTask.enter();
529
- if (!started) {
530
- _debugLog('[_asyncStartCall()] task failed early', {
925
+ // If there was no callback function, we're dealing with a sync function
926
+ // that was lifted as async without one, there is only the callee.
927
+ if (!callbackFn) {
928
+ _debugLog("[_asyncStartCall()] no callback, resolving w/ callee result", {
531
929
  taskID: preparedTask.id(),
532
- subtaskID: subtask.id(),
930
+ componentIdx: preparedTask.componentIdx(),
931
+ preparedTask,
932
+ stateNumber: preparedTask.taskState(),
933
+ isResolved: preparedTask.isResolved(),
934
+ callbackFn,
533
935
  });
534
- throw new Error("task failed to start");
936
+ preparedTask.resolve([callbackResult]);
535
937
  return;
536
938
  }
537
939
 
538
- // TODO: retrieve/pass along actual fn name the callback corresponds to
539
- // (at least something like `<lifted fn name>_callback`)
540
- const fnName = [
541
- '<task ',
542
- subtask.parentTaskID(),
543
- '/subtask ',
544
- subtask.id(),
545
- '/task ',
546
- preparedTask.id(),
547
- '>',
548
- ].join("");
940
+ let fnName = callbackFn.fnName;
941
+ if (!fnName) {
942
+ fnName = [
943
+ '<task ',
944
+ subtask.parentTaskID(),
945
+ '/subtask ',
946
+ subtask.id(),
947
+ '/task ',
948
+ preparedTask.id(),
949
+ '>',
950
+ ].join("");
951
+ }
549
952
 
550
953
  try {
551
- _debugLog("[_asyncStartCall()] starting driver loop", { fnName, componentIdx: preparedTask.componentIdx(), });
954
+ _debugLog("[_asyncStartCall()] starting driver loop", {
955
+ fnName,
956
+ componentIdx: preparedTask.componentIdx(),
957
+ subtaskID: subtask.id(),
958
+ childTaskID: subtask.childTaskID(),
959
+ parentTaskID: subtask.parentTaskID(),
960
+ });
961
+
552
962
  await _driverLoop({
553
963
  componentState: calleeComponentState,
554
964
  task: preparedTask,
@@ -564,6 +974,18 @@ function _asyncStartCall(args, callee, paramCount, resultCount, flags) {
564
974
 
565
975
  });
566
976
 
977
+ const subtaskState = subtask.getStateNumber();
978
+ if (subtaskState < 0 || subtaskState > 2**5) {
979
+ throw new Error('invalid subtask state, out of valid range');
980
+ }
981
+
982
+ _debugLog('[_asyncStartCall()] returning subtask rep & state', {
983
+ subtask: {
984
+ rep: subtask.waitableRep(),
985
+ state: subtaskState,
986
+ }
987
+ });
988
+
567
989
  return Number(subtask.waitableRep()) << 4 | subtaskState;
568
990
  }
569
991
 
@@ -572,612 +994,822 @@ function _syncStartCall(callbackIdx) {
572
994
  throw new Error('synchronous start call not implemented!');
573
995
  }
574
996
 
575
- let dv = new DataView(new ArrayBuffer());
576
- const dataView = mem => dv.buffer === mem.buffer ? dv : dv = new DataView(mem.buffer);
577
- const TEXT_DECODER_UTF8 = new TextDecoder();
578
- const TEXT_ENCODER_UTF8 = new TextEncoder();
579
-
580
- function _utf8AllocateAndEncode(s, realloc, memory) {
581
- if (typeof s !== 'string') {
582
- throw new TypeError('expected a string, received [' + typeof s + ']');
583
- }
584
- if (s.length === 0) { return { ptr: 1, len: 0 }; }
585
- let buf = TEXT_ENCODER_UTF8.encode(s);
586
- let ptr = realloc(0, 0, 1, buf.length);
587
- new Uint8Array(memory.buffer).set(buf, ptr);
588
- return { ptr, len: buf.length, codepoints: [...s].length };
589
- }
590
-
591
-
592
- function createNewCurrentTask(args) {
593
- _debugLog('[createNewCurrentTask()] args', args);
594
- const {
595
- componentIdx,
596
- isAsync,
597
- entryFnName,
598
- parentSubtaskID,
599
- callbackFnName,
600
- getCallbackFn,
601
- getParamsFn,
602
- stringEncoding,
603
- errHandling,
604
- getCalleeParamsFn,
605
- resultPtr,
606
- callingWasmExport,
607
- } = args;
608
- if (componentIdx === undefined || componentIdx === null) {
609
- throw new Error('missing/invalid component instance index while starting task');
610
- }
611
- const taskMetas = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
612
- const callbackFn = getCallbackFn ? getCallbackFn() : null;
997
+ class Waitable {
998
+ #componentIdx;
613
999
 
614
- const newTask = new AsyncTask({
615
- componentIdx,
616
- isAsync,
617
- entryFnName,
618
- callbackFn,
619
- callbackFnName,
620
- stringEncoding,
621
- getCalleeParamsFn,
622
- resultPtr,
623
- errHandling,
624
- });
1000
+ #pendingEventFn = null;
625
1001
 
626
- const newTaskID = newTask.id();
627
- const newTaskMeta = { id: newTaskID, componentIdx, task: newTask };
1002
+ #promise;
1003
+ #resolve;
1004
+ #reject;
628
1005
 
629
- ASYNC_CURRENT_TASK_IDS.push(newTaskID);
630
- ASYNC_CURRENT_COMPONENT_IDXS.push(componentIdx);
1006
+ #waitableSet = null;
631
1007
 
632
- if (!taskMetas) {
633
- ASYNC_TASKS_BY_COMPONENT_IDX.set(componentIdx, [newTaskMeta]);
634
- } else {
635
- taskMetas.push(newTaskMeta);
636
- }
1008
+ #idx = null; // to component-global waitables
637
1009
 
638
- return [newTask, newTaskID];
639
- }
640
-
641
- function endCurrentTask(componentIdx, taskID) {
642
- componentIdx ??= ASYNC_CURRENT_COMPONENT_IDXS.at(-1);
643
- taskID ??= ASYNC_CURRENT_TASK_IDS.at(-1);
644
- _debugLog('[endCurrentTask()] args', { componentIdx, taskID });
1010
+ target;
645
1011
 
646
- if (componentIdx === undefined || componentIdx === null) {
647
- throw new Error('missing/invalid component instance index while ending current task');
1012
+ constructor(args) {
1013
+ const { componentIdx, target } = args;
1014
+ this.#componentIdx = componentIdx;
1015
+ this.target = args.target;
1016
+ this.#resetPromise();
648
1017
  }
649
1018
 
650
- const tasks = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
651
- if (!tasks || !Array.isArray(tasks)) {
652
- throw new Error('missing/invalid tasks for component instance while ending task');
653
- }
654
- if (tasks.length == 0) {
655
- throw new Error('no current task(s) for component instance while ending task');
656
- }
1019
+ componentIdx() { return this.#componentIdx; }
1020
+ isInSet() { return this.#waitableSet !== null; }
657
1021
 
658
- if (taskID) {
659
- const last = tasks[tasks.length - 1];
660
- if (last.id !== taskID) {
661
- // throw new Error('current task does not match expected task ID');
662
- return;
663
- }
1022
+ idx() { return this.#idx; }
1023
+ setIdx(idx) {
1024
+ if (idx === 0) { throw new Error("waitable idx cannot be zero"); }
1025
+ this.#idx = idx;
664
1026
  }
665
1027
 
666
- ASYNC_CURRENT_TASK_IDS.pop();
667
- ASYNC_CURRENT_COMPONENT_IDXS.pop();
668
-
669
- const taskMeta = tasks.pop();
670
- return taskMeta.task;
671
- }
672
- const ASYNC_TASKS_BY_COMPONENT_IDX = new Map();
673
-
674
- class AsyncTask {
675
- static _ID = 0n;
676
-
677
- static State = {
678
- INITIAL: 'initial',
679
- CANCELLED: 'cancelled',
680
- CANCEL_PENDING: 'cancel-pending',
681
- CANCEL_DELIVERED: 'cancel-delivered',
682
- RESOLVED: 'resolved',
683
- }
1028
+ setTarget(tgt) { this.target = tgt; }
684
1029
 
685
- static BlockResult = {
686
- CANCELLED: 'block.cancelled',
687
- NOT_CANCELLED: 'block.not-cancelled',
1030
+ #resetPromise() {
1031
+ const { promise, resolve, reject } = promiseWithResolvers()
1032
+ this.#promise = promise;
1033
+ this.#resolve = resolve;
1034
+ this.#reject = reject;
688
1035
  }
689
1036
 
690
- #id;
691
- #componentIdx;
692
- #state;
693
- #isAsync;
694
- #entryFnName = null;
695
- #subtasks = [];
696
-
697
- #onResolveHandlers = [];
698
- #completionPromise = null;
699
-
700
- #memoryIdx = null;
701
-
702
- #callbackFn = null;
703
- #callbackFnName = null;
704
-
705
- #postReturnFn = null;
706
-
707
- #getCalleeParamsFn = null;
708
-
709
- #stringEncoding = null;
710
-
711
- #parentSubtask = null;
1037
+ resolve() { this.#resolve(); }
1038
+ reject(err) { this.#reject(err); }
1039
+ promise() { return this.#promise; }
712
1040
 
713
- #needsExclusiveLock = false;
714
-
715
- #errHandling;
1041
+ hasPendingEvent() {
1042
+ // _debugLog('[Waitable#hasPendingEvent()]', {
1043
+ // componentIdx: this.#componentIdx,
1044
+ // waitable: this,
1045
+ // waitableSet: this.#waitableSet,
1046
+ // hasPendingEvent: this.#pendingEventFn !== null,
1047
+ // });
1048
+ return this.#pendingEventFn !== null;
1049
+ }
1050
+
1051
+ setPendingEvent(fn) {
1052
+ _debugLog('[Waitable#setPendingEvent()] args', {
1053
+ waitable: this,
1054
+ inSet: this.#waitableSet,
1055
+ });
1056
+ this.#pendingEventFn = fn;
1057
+ }
1058
+
1059
+ getPendingEvent() {
1060
+ _debugLog('[Waitable#getPendingEvent()] args', {
1061
+ waitable: this,
1062
+ inSet: this.#waitableSet,
1063
+ hasPendingEvent: this.#pendingEventFn !== null,
1064
+ });
1065
+ if (this.#pendingEventFn === null) { return null; }
1066
+ const eventFn = this.#pendingEventFn;
1067
+ this.#pendingEventFn = null;
1068
+ const e = eventFn();
1069
+ this.#resetPromise();
1070
+ return e;
1071
+ }
1072
+
1073
+ join(waitableSet) {
1074
+ _debugLog('[Waitable#join()] args', {
1075
+ waitable: this,
1076
+ waitableSet: waitableSet,
1077
+ });
1078
+ if (this.#waitableSet) { this.#waitableSet.removeWaitable(this); }
1079
+ if (!waitableSet) {
1080
+ this.#waitableSet = null;
1081
+ return;
1082
+ }
1083
+ waitableSet.addWaitable(this);
1084
+ this.#waitableSet = waitableSet;
1085
+ }
1086
+
1087
+ drop() {
1088
+ _debugLog('[Waitable#drop()] args', {
1089
+ componentIdx: this.#componentIdx,
1090
+ waitable: this,
1091
+ });
1092
+ if (this.hasPendingEvent()) {
1093
+ throw new Error('waitables with pending events cannot be dropped');
1094
+ }
1095
+ this.join(null);
1096
+ }
1097
+
1098
+ }
716
1099
 
717
- #backpressurePromise;
718
- #backpressureWaiters = 0n;
1100
+ const ERR_CTX_TABLES = {};
719
1101
 
720
- #returnLowerFns = null;
1102
+ let dv = new DataView(new ArrayBuffer());
1103
+ const dataView = mem => dv.buffer === mem.buffer ? dv : dv = new DataView(mem.buffer);
1104
+ const TEXT_DECODER_UTF8 = new TextDecoder();
1105
+ const TEXT_ENCODER_UTF8 = new TextEncoder();
721
1106
 
722
- cancelled = false;
723
- requested = false;
724
- alwaysTaskReturn = false;
1107
+ function _utf8AllocateAndEncode(s, realloc, memory) {
1108
+ if (typeof s !== 'string') {
1109
+ throw new TypeError('expected a string, received [' + typeof s + ']');
1110
+ }
1111
+ if (s.length === 0) { return { ptr: 1, len: 0 }; }
1112
+ let buf = TEXT_ENCODER_UTF8.encode(s);
1113
+ let ptr = realloc(0, 0, 1, buf.length);
1114
+ new Uint8Array(memory.buffer).set(buf, ptr);
1115
+ const res = { ptr, len: buf.length, codepoints: [...s].length };
1116
+ return res;
1117
+ }
725
1118
 
726
- returnCalls = 0;
727
- storage = [0, 0];
728
- borrowedHandles = {};
729
1119
 
730
- awaitableResume = null;
731
- awaitableCancel = null;
1120
+ function createNewCurrentTask(args) {
1121
+ _debugLog('[createNewCurrentTask()] args', args);
1122
+ const {
1123
+ componentIdx,
1124
+ isAsync,
1125
+ isManualAsync,
1126
+ entryFnName,
1127
+ parentSubtaskID,
1128
+ callbackFnName,
1129
+ getCallbackFn,
1130
+ getParamsFn,
1131
+ stringEncoding,
1132
+ errHandling,
1133
+ getCalleeParamsFn,
1134
+ resultPtr,
1135
+ callingWasmExport,
1136
+ } = args;
1137
+ if (componentIdx === undefined || componentIdx === null) {
1138
+ throw new Error('missing/invalid component instance index while starting task');
1139
+ }
1140
+ let taskMetas = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx);
1141
+ const callbackFn = getCallbackFn ? getCallbackFn() : null;
1142
+
1143
+ const newTask = new AsyncTask({
1144
+ componentIdx,
1145
+ isAsync,
1146
+ isManualAsync,
1147
+ entryFnName,
1148
+ callbackFn,
1149
+ callbackFnName,
1150
+ stringEncoding,
1151
+ getCalleeParamsFn,
1152
+ resultPtr,
1153
+ errHandling,
1154
+ });
1155
+
1156
+ const newTaskID = newTask.id();
1157
+ const newTaskMeta = { id: newTaskID, componentIdx, task: newTask };
1158
+
1159
+ // NOTE: do not track host tasks
1160
+ ASYNC_CURRENT_TASK_IDS.push(newTaskID);
1161
+ ASYNC_CURRENT_COMPONENT_IDXS.push(componentIdx);
1162
+
1163
+ if (!taskMetas) {
1164
+ taskMetas = [newTaskMeta];
1165
+ ASYNC_TASKS_BY_COMPONENT_IDX.set(componentIdx, [newTaskMeta]);
1166
+ } else {
1167
+ taskMetas.push(newTaskMeta);
1168
+ }
1169
+
1170
+ return [newTask, newTaskID];
1171
+ }
1172
+ const ASYNC_TASKS_BY_COMPONENT_IDX = new Map();
732
1173
 
733
- constructor(opts) {
734
- this.#id = ++AsyncTask._ID;
1174
+ class AsyncTask {
1175
+ static _ID = 0n;
735
1176
 
736
- if (opts?.componentIdx === undefined) {
737
- throw new TypeError('missing component id during task creation');
1177
+ static State = {
1178
+ INITIAL: 'initial',
1179
+ CANCELLED: 'cancelled',
1180
+ CANCEL_PENDING: 'cancel-pending',
1181
+ CANCEL_DELIVERED: 'cancel-delivered',
1182
+ RESOLVED: 'resolved',
738
1183
  }
739
- this.#componentIdx = opts.componentIdx;
740
1184
 
741
- this.#state = AsyncTask.State.INITIAL;
742
- this.#isAsync = opts?.isAsync ?? false;
743
- this.#entryFnName = opts.entryFnName;
1185
+ static BlockResult = {
1186
+ CANCELLED: 'block.cancelled',
1187
+ NOT_CANCELLED: 'block.not-cancelled',
1188
+ }
744
1189
 
745
- const {
746
- promise: completionPromise,
747
- resolve: resolveCompletionPromise,
748
- reject: rejectCompletionPromise,
749
- } = promiseWithResolvers();
750
- this.#completionPromise = completionPromise;
1190
+ #id;
1191
+ #componentIdx;
1192
+ #state;
1193
+ #isAsync;
1194
+ #isManualAsync;
1195
+ #entryFnName = null;
751
1196
 
752
- this.#onResolveHandlers.push((results) => {
753
- resolveCompletionPromise(results);
754
- })
1197
+ #onResolveHandlers = [];
1198
+ #completionPromise = null;
1199
+ #rejected = false;
755
1200
 
756
- if (opts.callbackFn) { this.#callbackFn = opts.callbackFn; }
757
- if (opts.callbackFnName) { this.#callbackFnName = opts.callbackFnName; }
1201
+ #exitPromise = null;
1202
+ #onExitHandlers = [];
758
1203
 
759
- if (opts.getCalleeParamsFn) { this.#getCalleeParamsFn = opts.getCalleeParamsFn; }
1204
+ #memoryIdx = null;
1205
+ #memory = null;
760
1206
 
761
- if (opts.stringEncoding) { this.#stringEncoding = opts.stringEncoding; }
1207
+ #callbackFn = null;
1208
+ #callbackFnName = null;
762
1209
 
763
- if (opts.parentSubtask) { this.#parentSubtask = opts.parentSubtask; }
1210
+ #postReturnFn = null;
764
1211
 
765
- this.#needsExclusiveLock = this.isSync() || !this.hasCallback();
1212
+ #getCalleeParamsFn = null;
766
1213
 
767
- if (opts.errHandling) { this.#errHandling = opts.errHandling; }
768
- }
769
-
770
- taskState() { return this.#state; }
771
- id() { return this.#id; }
772
- componentIdx() { return this.#componentIdx; }
773
- isAsync() { return this.#isAsync; }
774
- entryFnName() { return this.#entryFnName; }
775
- completionPromise() { return this.#completionPromise; }
776
-
777
- isAsync() { return this.#isAsync; }
778
- isSync() { return !this.isAsync(); }
779
-
780
- getErrHandling() { return this.#errHandling; }
781
-
782
- hasCallback() { return this.#callbackFn !== null; }
783
-
784
- setReturnMemoryIdx(idx) { this.#memoryIdx = idx; }
785
- getReturnMemoryIdx() { return this.#memoryIdx; }
786
-
787
- setReturnLowerFns(fns) { this.#returnLowerFns = fns; }
788
- getReturnLowerFns() { return this.#returnLowerFns; }
789
-
790
- setParentSubtask(subtask) {
791
- if (!subtask || !(subtask instanceof AsyncSubtask)) { return }
792
- if (this.#parentSubtask) { throw new Error('parent subtask can only be set once'); }
793
- this.#parentSubtask = subtask;
794
- }
795
-
796
- getParentSubtask() { return this.#parentSubtask; }
797
-
798
- // TODO(threads): this is very inefficient, we can pass along a root task,
799
- // and ideally do not need this once thread support is in place
800
- getRootTask() {
801
- let currentSubtask = this.getParentSubtask();
802
- let task = this;
803
- while (currentSubtask) {
804
- task = currentSubtask.getParentTask();
805
- currentSubtask = task.getParentSubtask();
1214
+ #stringEncoding = null;
1215
+
1216
+ #parentSubtask = null;
1217
+
1218
+ #needsExclusiveLock = false;
1219
+
1220
+ #errHandling;
1221
+
1222
+ #backpressurePromise;
1223
+ #backpressureWaiters = 0n;
1224
+
1225
+ #returnLowerFns = null;
1226
+
1227
+ #subtasks = [];
1228
+
1229
+ #entered = false;
1230
+ #exited = false;
1231
+ #errored = null;
1232
+
1233
+ cancelled = false;
1234
+ cancelRequested = false;
1235
+ alwaysTaskReturn = false;
1236
+
1237
+ returnCalls = 0;
1238
+ storage = [0, 0];
1239
+ borrowedHandles = {};
1240
+
1241
+ constructor(opts) {
1242
+ this.#id = ++AsyncTask._ID;
1243
+
1244
+ if (opts?.componentIdx === undefined) {
1245
+ throw new TypeError('missing component id during task creation');
1246
+ }
1247
+ this.#componentIdx = opts.componentIdx;
1248
+
1249
+ this.#state = AsyncTask.State.INITIAL;
1250
+ this.#isAsync = opts?.isAsync ?? false;
1251
+ this.#isManualAsync = opts?.isManualAsync ?? false;
1252
+ this.#entryFnName = opts.entryFnName;
1253
+
1254
+ const {
1255
+ promise: completionPromise,
1256
+ resolve: resolveCompletionPromise,
1257
+ reject: rejectCompletionPromise,
1258
+ } = promiseWithResolvers();
1259
+ this.#completionPromise = completionPromise;
1260
+
1261
+ this.#onResolveHandlers.push((results) => {
1262
+ if (this.#errored !== null) {
1263
+ rejectCompletionPromise(this.#errored);
1264
+ return;
1265
+ } else if (this.#rejected) {
1266
+ rejectCompletionPromise(results);
1267
+ return;
1268
+ }
1269
+ resolveCompletionPromise(results);
1270
+ });
1271
+
1272
+ const {
1273
+ promise: exitPromise,
1274
+ resolve: resolveExitPromise,
1275
+ reject: rejectExitPromise,
1276
+ } = promiseWithResolvers();
1277
+ this.#exitPromise = exitPromise;
1278
+
1279
+ this.#onExitHandlers.push(() => {
1280
+ resolveExitPromise();
1281
+ });
1282
+
1283
+ if (opts.callbackFn) { this.#callbackFn = opts.callbackFn; }
1284
+ if (opts.callbackFnName) { this.#callbackFnName = opts.callbackFnName; }
1285
+
1286
+ if (opts.getCalleeParamsFn) { this.#getCalleeParamsFn = opts.getCalleeParamsFn; }
1287
+
1288
+ if (opts.stringEncoding) { this.#stringEncoding = opts.stringEncoding; }
1289
+
1290
+ if (opts.parentSubtask) { this.#parentSubtask = opts.parentSubtask; }
1291
+
1292
+ this.#needsExclusiveLock = this.isSync() || !this.hasCallback();
1293
+
1294
+ if (opts.errHandling) { this.#errHandling = opts.errHandling; }
806
1295
  }
807
- return task;
808
- }
809
-
810
- setPostReturnFn(f) {
811
- if (!f) { return; }
812
- if (this.#postReturnFn) { throw new Error('postReturn fn can only be set once'); }
813
- this.#postReturnFn = f;
814
- }
815
-
816
- setCallbackFn(f, name) {
817
- if (!f) { return; }
818
- if (this.#callbackFn) { throw new Error('callback fn can only be set once'); }
819
- this.#callbackFn = f;
820
- this.#callbackFnName = name;
821
- }
822
-
823
- getCallbackFnName() {
824
- if (!this.#callbackFnName) { return undefined; }
825
- return this.#callbackFnName;
826
- }
827
-
828
- runCallbackFn(...args) {
829
- if (!this.#callbackFn) { throw new Error('on callback function has been set for task'); }
830
- return this.#callbackFn.apply(null, args);
831
- }
832
-
833
- getCalleeParams() {
834
- if (!this.#getCalleeParamsFn) { throw new Error('missing/invalid getCalleeParamsFn'); }
835
- return this.#getCalleeParamsFn();
836
- }
837
-
838
- mayEnter(task) {
839
- const cstate = getOrCreateAsyncState(this.#componentIdx);
840
- if (cstate.hasBackpressure()) {
841
- _debugLog('[AsyncTask#mayEnter()] disallowed due to backpressure', { taskID: this.#id });
842
- return false;
1296
+
1297
+ taskState() { return this.#state; }
1298
+ id() { return this.#id; }
1299
+ componentIdx() { return this.#componentIdx; }
1300
+ entryFnName() { return this.#entryFnName; }
1301
+
1302
+ completionPromise() { return this.#completionPromise; }
1303
+ exitPromise() { return this.#exitPromise; }
1304
+
1305
+ isAsync() { return this.#isAsync; }
1306
+ isSync() { return !this.isAsync(); }
1307
+
1308
+ getErrHandling() { return this.#errHandling; }
1309
+
1310
+ hasCallback() { return this.#callbackFn !== null; }
1311
+
1312
+ getReturnMemoryIdx() { return this.#memoryIdx; }
1313
+ setReturnMemoryIdx(idx) {
1314
+ if (idx === null) { return; }
1315
+ this.#memoryIdx = idx;
843
1316
  }
844
- if (!cstate.callingSyncImport()) {
845
- _debugLog('[AsyncTask#mayEnter()] disallowed due to sync import call', { taskID: this.#id });
846
- return false;
1317
+
1318
+ getReturnMemory() { return this.#memory; }
1319
+ setReturnMemory(m) {
1320
+ if (m === null) { return; }
1321
+ this.#memory = m;
847
1322
  }
848
- const callingSyncExportWithSyncPending = cstate.callingSyncExport && !task.isAsync;
849
- if (!callingSyncExportWithSyncPending) {
850
- _debugLog('[AsyncTask#mayEnter()] disallowed due to sync export w/ sync pending', { taskID: this.#id });
851
- return false;
1323
+
1324
+ setReturnLowerFns(fns) { this.#returnLowerFns = fns; }
1325
+ getReturnLowerFns() { return this.#returnLowerFns; }
1326
+
1327
+ setParentSubtask(subtask) {
1328
+ if (!subtask || !(subtask instanceof AsyncSubtask)) { return }
1329
+ if (this.#parentSubtask) { throw new Error('parent subtask can only be set once'); }
1330
+ this.#parentSubtask = subtask;
1331
+ }
1332
+
1333
+ getParentSubtask() { return this.#parentSubtask; }
1334
+
1335
+ // TODO(threads): this is very inefficient, we can pass along a root task,
1336
+ // and ideally do not need this once thread support is in place
1337
+ getRootTask() {
1338
+ let currentSubtask = this.getParentSubtask();
1339
+ let task = this;
1340
+ while (currentSubtask) {
1341
+ task = currentSubtask.getParentTask();
1342
+ currentSubtask = task.getParentSubtask();
1343
+ }
1344
+ return task;
1345
+ }
1346
+
1347
+ setPostReturnFn(f) {
1348
+ if (!f) { return; }
1349
+ if (this.#postReturnFn) { throw new Error('postReturn fn can only be set once'); }
1350
+ this.#postReturnFn = f;
1351
+ }
1352
+
1353
+ setCallbackFn(f, name) {
1354
+ if (!f) { return; }
1355
+ if (this.#callbackFn) { throw new Error('callback fn can only be set once'); }
1356
+ this.#callbackFn = f;
1357
+ this.#callbackFnName = name;
1358
+ }
1359
+
1360
+ getCallbackFnName() {
1361
+ if (!this.#callbackFnName) { return undefined; }
1362
+ return this.#callbackFnName;
1363
+ }
1364
+
1365
+ async runCallbackFn(...args) {
1366
+ if (!this.#callbackFn) { throw new Error('on callback function has been set for task'); }
1367
+ return await this.#callbackFn.apply(null, args);
1368
+ }
1369
+
1370
+ getCalleeParams() {
1371
+ if (!this.#getCalleeParamsFn) { throw new Error('missing/invalid getCalleeParamsFn'); }
1372
+ return this.#getCalleeParamsFn();
1373
+ }
1374
+
1375
+ mayBlock() { return this.isAsync() || this.isResolvedState() }
1376
+
1377
+ mayEnter(task) {
1378
+ const cstate = getOrCreateAsyncState(this.#componentIdx);
1379
+ if (cstate.hasBackpressure()) {
1380
+ _debugLog('[AsyncTask#mayEnter()] disallowed due to backpressure', { taskID: this.#id });
1381
+ return false;
1382
+ }
1383
+ if (!cstate.callingSyncImport()) {
1384
+ _debugLog('[AsyncTask#mayEnter()] disallowed due to sync import call', { taskID: this.#id });
1385
+ return false;
1386
+ }
1387
+ const callingSyncExportWithSyncPending = cstate.callingSyncExport && !task.isAsync;
1388
+ if (!callingSyncExportWithSyncPending) {
1389
+ _debugLog('[AsyncTask#mayEnter()] disallowed due to sync export w/ sync pending', { taskID: this.#id });
1390
+ return false;
1391
+ }
1392
+ return true;
1393
+ }
1394
+
1395
+ enterSync() {
1396
+ if (this.needsExclusiveLock()) {
1397
+ const cstate = getOrCreateAsyncState(this.#componentIdx);
1398
+ cstate.exclusiveLock();
1399
+ }
1400
+ return true;
1401
+ }
1402
+
1403
+ async enter(opts) {
1404
+ _debugLog('[AsyncTask#enter()] args', {
1405
+ taskID: this.#id,
1406
+ componentIdx: this.#componentIdx,
1407
+ subtaskID: this.getParentSubtask()?.id(),
1408
+ });
1409
+
1410
+ if (this.#entered) {
1411
+ throw new Error(`task with ID [${this.#id}] should not be entered twice`);
1412
+ }
1413
+
1414
+ const cstate = getOrCreateAsyncState(this.#componentIdx);
1415
+
1416
+ // If a task is either synchronous or host-provided (e.g. a host import, whether sync or async)
1417
+ // then we can avoid component-relevant tracking and immediately enter
1418
+ if (this.isSync() || opts?.isHost) {
1419
+ this.#entered = true;
1420
+
1421
+ // TODO(breaking): remove once manually-spccifying async fns is removed
1422
+ // It is currently possible for an actually sync export to be specified
1423
+ // as async via JSPI
1424
+ if (this.#isManualAsync) {
1425
+ if (this.needsExclusiveLock()) { cstate.exclusiveLock(); }
1426
+ }
1427
+
1428
+ return this.#entered;
1429
+ }
1430
+
1431
+ if (cstate.hasBackpressure()) {
1432
+ cstate.addBackpressureWaiter();
1433
+
1434
+ const result = await this.waitUntil({
1435
+ readyFn: () => !cstate.hasBackpressure(),
1436
+ cancellable: true,
1437
+ });
1438
+
1439
+ cstate.removeBackpressureWaiter();
1440
+
1441
+ if (result === AsyncTask.BlockResult.CANCELLED) {
1442
+ this.cancel();
1443
+ return false;
1444
+ }
1445
+ }
1446
+
1447
+ if (this.needsExclusiveLock()) { cstate.exclusiveLock(); }
1448
+
1449
+ this.#entered = true;
1450
+ return this.#entered;
852
1451
  }
853
- return true;
854
- }
855
-
856
- async enter() {
857
- _debugLog('[AsyncTask#enter()] args', { taskID: this.#id });
858
- const cstate = getOrCreateAsyncState(this.#componentIdx);
859
1452
 
860
- if (this.isSync()) { return true; }
1453
+ isRunningState() { return this.#state !== AsyncTask.State.RESOLVED; }
1454
+ isResolvedState() { return this.#state === AsyncTask.State.RESOLVED; }
1455
+ isResolved() { return this.#state === AsyncTask.State.RESOLVED; }
861
1456
 
862
- if (cstate.hasBackpressure()) {
863
- cstate.addBackpressureWaiter();
1457
+ async waitUntil(opts) {
1458
+ const { readyFn, waitableSetRep, cancellable } = opts;
1459
+ _debugLog('[AsyncTask#waitUntil()] args', { taskID: this.#id, waitableSetRep, cancellable });
864
1460
 
865
- const result = await this.waitUntil({
866
- readyFn: () => !cstate.hasBackpressure(),
867
- cancellable: true,
868
- });
1461
+ const state = getOrCreateAsyncState(this.#componentIdx);
1462
+ const wset = state.handles.get(waitableSetRep);
869
1463
 
870
- cstate.removeBackpressureWaiter();
1464
+ let event;
871
1465
 
872
- if (result === AsyncTask.BlockResult.CANCELLED) {
873
- this.cancel();
874
- return false;
1466
+ wset.incrementNumWaiting();
1467
+
1468
+ const keepGoing = await this.suspendUntil({
1469
+ readyFn: () => {
1470
+ const hasPendingEvent = wset.hasPendingEvent();
1471
+ const ready = readyFn();
1472
+ return ready && hasPendingEvent;
1473
+ },
1474
+ cancellable,
1475
+ });
1476
+
1477
+ if (keepGoing) {
1478
+ event = wset.getPendingEvent();
1479
+ } else {
1480
+ event = {
1481
+ code: ASYNC_EVENT_CODE.TASK_CANCELLED,
1482
+ payload0: 0,
1483
+ payload1: 0,
1484
+ };
875
1485
  }
1486
+
1487
+ wset.decrementNumWaiting();
1488
+
1489
+ return event;
876
1490
  }
877
1491
 
878
- if (this.needsExclusiveLock()) { cstate.exclusiveLock(); }
1492
+ async yieldUntil(opts) {
1493
+ const { readyFn, cancellable } = opts;
1494
+ _debugLog('[AsyncTask#yieldUntil()] args', { taskID: this.#id, cancellable });
1495
+
1496
+ const keepGoing = await this.suspendUntil({ readyFn, cancellable });
1497
+ if (keepGoing) {
1498
+ return {
1499
+ code: ASYNC_EVENT_CODE.NONE,
1500
+ payload0: 0,
1501
+ payload1: 0,
1502
+ };
1503
+ }
1504
+
1505
+ return {
1506
+ code: ASYNC_EVENT_CODE.TASK_CANCELLED,
1507
+ payload0: 0,
1508
+ payload1: 0,
1509
+ };
1510
+ }
879
1511
 
880
- return true;
881
- }
882
-
883
- isRunning() {
884
- return this.#state !== AsyncTask.State.RESOLVED;
885
- }
886
-
887
- async waitUntil(opts) {
888
- const { readyFn, waitableSetRep, cancellable } = opts;
889
- _debugLog('[AsyncTask#waitUntil()] args', { taskID: this.#id, waitableSetRep, cancellable });
1512
+ async suspendUntil(opts) {
1513
+ const { cancellable, readyFn } = opts;
1514
+ _debugLog('[AsyncTask#suspendUntil()] args', { cancellable });
1515
+
1516
+ const pendingCancelled = this.deliverPendingCancel({ cancellable });
1517
+ if (pendingCancelled) { return false; }
1518
+
1519
+ const completed = await this.immediateSuspendUntil({ readyFn, cancellable });
1520
+ return completed;
1521
+ }
890
1522
 
891
- const state = getOrCreateAsyncState(this.#componentIdx);
892
- const wset = state.waitableSets.get(waitableSetRep);
1523
+ // TODO(threads): equivalent to thread.suspend_until()
1524
+ async immediateSuspendUntil(opts) {
1525
+ const { cancellable, readyFn } = opts;
1526
+ _debugLog('[AsyncTask#immediateSuspendUntil()] args', { cancellable, readyFn });
1527
+
1528
+ const ready = readyFn();
1529
+ if (ready && ASYNC_DETERMINISM === 'random') {
1530
+ const coinFlip = _coinFlip();
1531
+ if (coinFlip) { return true }
1532
+ }
1533
+
1534
+ const keepGoing = await this.immediateSuspend({ cancellable, readyFn });
1535
+ return keepGoing;
1536
+ }
893
1537
 
894
- let event;
1538
+ async immediateSuspend(opts) { // NOTE: equivalent to thread.suspend()
1539
+ // TODO(threads): store readyFn on the thread
1540
+ const { cancellable, readyFn } = opts;
1541
+ _debugLog('[AsyncTask#immediateSuspend()] args', { cancellable, readyFn });
895
1542
 
896
- wset.incrementNumWaiting();
1543
+ const pendingCancelled = this.deliverPendingCancel({ cancellable });
1544
+ if (pendingCancelled) { return false; }
897
1545
 
898
- const keepGoing = await this.suspendUntil({
899
- readyFn: () => {
900
- const hasPendingEvent = wset.hasPendingEvent();
901
- return readyFn() && hasPendingEvent;
902
- },
903
- cancellable,
904
- });
1546
+ const cstate = getOrCreateAsyncState(this.#componentIdx);
1547
+ const keepGoing = await cstate.suspendTask({ task: this, readyFn });
1548
+ return keepGoing;
1549
+ }
1550
+
1551
+ deliverPendingCancel(opts) {
1552
+ const { cancellable } = opts;
1553
+ _debugLog('[AsyncTask#deliverPendingCancel()] args', { cancellable });
905
1554
 
906
- if (keepGoing) {
907
- event = wset.getPendingEvent();
908
- } else {
909
- event = {
910
- code: ASYNC_EVENT_CODE.TASK_CANCELLED,
911
- index: 0,
912
- result: 0,
913
- };
1555
+ if (cancellable && this.#state === AsyncTask.State.PENDING_CANCEL) {
1556
+ this.#state = AsyncTask.State.CANCEL_DELIVERED;
1557
+ return true;
914
1558
  }
915
1559
 
916
- wset.decrementNumWaiting();
917
-
918
- return event;
1560
+ return false;
1561
+ }
1562
+
1563
+ isCancelled() { return this.cancelled }
1564
+
1565
+ cancel(args) {
1566
+ _debugLog('[AsyncTask#cancel()] args', { });
1567
+ if (this.taskState() !== AsyncTask.State.CANCEL_DELIVERED) {
1568
+ throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] invalid task state [${this.taskState()}] for cancellation`);
1569
+ }
1570
+ if (this.borrowedHandles.length > 0) { throw new Error('task still has borrow handles'); }
1571
+ this.cancelled = true;
1572
+ this.onResolve(args?.error ?? new Error('task cancelled'));
1573
+ this.#state = AsyncTask.State.RESOLVED;
919
1574
  }
920
1575
 
921
- async onBlock(awaitable) {
922
- _debugLog('[AsyncTask#onBlock()] args', { taskID: this.#id, awaitable });
923
- if (!(awaitable instanceof Awaitable)) {
924
- throw new Error('invalid awaitable during onBlock');
1576
+ onResolve(taskValue) {
1577
+ const handlers = this.#onResolveHandlers;
1578
+ this.#onResolveHandlers = [];
1579
+ for (const f of handlers) {
1580
+ try {
1581
+ // TODO(fix): resolve handlers getting called a ton?
1582
+ f(taskValue);
1583
+ } catch (err) {
1584
+ _debugLog("[AsyncTask#onResolve] error during task resolve handler", err);
1585
+ throw err;
1586
+ }
925
1587
  }
926
1588
 
927
- // Build a promise that this task can await on which resolves when it is awoken
928
- const { promise, resolve, reject } = promiseWithResolvers();
929
- this.awaitableResume = () => {
930
- _debugLog('[AsyncTask] resuming after onBlock', { taskID: this.#id });
931
- resolve();
932
- };
933
- this.awaitableCancel = (err) => {
934
- _debugLog('[AsyncTask] rejecting after onBlock', { taskID: this.#id, err });
935
- reject(err);
936
- };
1589
+ if (this.#parentSubtask) {
1590
+ const meta = this.#parentSubtask.getCallMetadata();
1591
+ // Run the rturn fn if it has not already been called -- this *should* have happened in
1592
+ // `task.return`, but some paths do not go through task.return (e.g. async lower of sync fn
1593
+ // which goes through prepare + async-start-call)
1594
+ if (meta.returnFn && !meta.returnFnCalled) {
1595
+ _debugLog('[AsyncTask#onResolve()] running returnFn', {
1596
+ componentIdx: this.#componentIdx,
1597
+ taskID: this.#id,
1598
+ subtaskID: this.#parentSubtask.id(),
1599
+ });
1600
+ const memory = meta.getMemoryFn();
1601
+ meta.returnFn.apply(null, [taskValue, meta.resultPtr]);
1602
+ meta.returnFnCalled = true;
1603
+ }
1604
+ }
937
1605
 
938
- // Park this task/execution to be handled later
939
- const state = getOrCreateAsyncState(this.#componentIdx);
940
- state.parkTaskOnAwaitable({ awaitable, task: this });
1606
+ if (this.#postReturnFn) {
1607
+ _debugLog('[AsyncTask#onResolve()] running post return ', {
1608
+ componentIdx: this.#componentIdx,
1609
+ taskID: this.#id,
1610
+ });
1611
+ try {
1612
+ this.#postReturnFn(taskValue);
1613
+ } catch (err) {
1614
+ _debugLog("[AsyncTask#onResolve] error during task resolve handler", err);
1615
+ throw err;
1616
+ }
1617
+ }
941
1618
 
942
- try {
943
- await promise;
944
- return AsyncTask.BlockResult.NOT_CANCELLED;
945
- } catch (err) {
946
- // rejection means task cancellation
947
- return AsyncTask.BlockResult.CANCELLED;
1619
+ if (this.#parentSubtask) {
1620
+ this.#parentSubtask.onResolve(taskValue);
948
1621
  }
949
1622
  }
950
1623
 
951
- async asyncOnBlock(awaitable) {
952
- _debugLog('[AsyncTask#asyncOnBlock()] args', { taskID: this.#id, awaitable });
953
- if (!(awaitable instanceof Awaitable)) {
954
- throw new Error('invalid awaitable during onBlock');
955
- }
956
- // TODO: watch for waitable AND cancellation
957
- // TODO: if it WAS cancelled:
958
- // - return true
959
- // - only once per subtask
960
- // - do not wait on the scheduler
961
- // - control flow should go to the subtask (only once)
962
- // - Once subtask blocks/resolves, reqlinquishControl() will tehn resolve request_cancel_end (without scheduler lock release)
963
- // - control flow goes back to request_cancel
964
- //
965
- // Subtask cancellation should work similarly to an async import call -- runs sync up until
966
- // the subtask blocks or resolves
967
- //
968
- throw new Error('AsyncTask#asyncOnBlock() not yet implemented');
1624
+ registerOnResolveHandler(f) {
1625
+ this.#onResolveHandlers.push(f);
1626
+ }
1627
+
1628
+ isRejected() { return this.#rejected; }
1629
+
1630
+ setErrored(err) {
1631
+ this.#errored = err;
969
1632
  }
970
1633
 
971
- async yieldUntil(opts) {
972
- const { readyFn, cancellable } = opts;
973
- _debugLog('[AsyncTask#yieldUntil()] args', { taskID: this.#id, cancellable });
1634
+ reject(taskErr) {
1635
+ _debugLog('[AsyncTask#reject()] args', {
1636
+ componentIdx: this.#componentIdx,
1637
+ taskID: this.#id,
1638
+ parentSubtask: this.#parentSubtask,
1639
+ parentSubtaskID: this.#parentSubtask?.id(),
1640
+ entryFnName: this.entryFnName(),
1641
+ callbackFnName: this.#callbackFnName,
1642
+ errMsg: taskErr.message,
1643
+ });
974
1644
 
975
- const keepGoing = await this.suspendUntil({ readyFn, cancellable });
976
- if (!keepGoing) {
977
- return {
978
- code: ASYNC_EVENT_CODE.TASK_CANCELLED,
979
- index: 0,
980
- result: 0,
981
- };
1645
+ if (this.isResolvedState() || this.#rejected) { return; }
1646
+
1647
+ for (const subtask of this.#subtasks) {
1648
+ subtask.reject(taskErr);
982
1649
  }
983
1650
 
984
- return {
985
- code: ASYNC_EVENT_CODE.NONE,
986
- index: 0,
987
- result: 0,
988
- };
989
- }
990
-
991
- async suspendUntil(opts) {
992
- const { cancellable, readyFn } = opts;
993
- _debugLog('[AsyncTask#suspendUntil()] args', { cancellable });
1651
+ this.#rejected = true;
1652
+ this.cancelRequested = true;
1653
+ this.#state = AsyncTask.State.PENDING_CANCEL;
1654
+ const cancelled = this.deliverPendingCancel({ cancellable: true });
1655
+
1656
+ // TODO: do cleanup here to reset the machinery so we can run again?
994
1657
 
995
- const pendingCancelled = this.deliverPendingCancel({ cancellable });
996
- if (pendingCancelled) { return false; }
997
1658
 
998
- const completed = await this.immediateSuspendUntil({ readyFn, cancellable });
999
- return completed;
1659
+ this.cancel({ error: taskErr });
1000
1660
  }
1001
1661
 
1002
- // TODO(threads): equivalent to thread.suspend_until()
1003
- async immediateSuspendUntil(opts) {
1004
- const { cancellable, readyFn } = opts;
1005
- _debugLog('[AsyncTask#immediateSuspendUntil()] args', { cancellable, readyFn });
1662
+ resolve(results) {
1663
+ _debugLog('[AsyncTask#resolve()] args', {
1664
+ componentIdx: this.#componentIdx,
1665
+ taskID: this.#id,
1666
+ entryFnName: this.entryFnName(),
1667
+ callbackFnName: this.#callbackFnName,
1668
+ });
1006
1669
 
1007
- const ready = readyFn();
1008
- if (ready && !ASYNC_DETERMINISM && _coinFlip()) {
1009
- return true;
1670
+ if (this.#state === AsyncTask.State.RESOLVED) {
1671
+ throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] is already resolved (did you forget to wait for an import?)`);
1010
1672
  }
1011
1673
 
1012
- const cstate = getOrCreateAsyncState(this.#componentIdx);
1013
- cstate.addPendingTask(this);
1674
+ if (this.borrowedHandles.length > 0) {
1675
+ throw new Error('task still has borrow handles');
1676
+ }
1014
1677
 
1015
- const keepGoing = await this.immediateSuspend({ cancellable, readyFn });
1016
- return keepGoing;
1017
- }
1018
-
1019
- async immediateSuspend(opts) { // NOTE: equivalent to thread.suspend()
1020
- // TODO(threads): store readyFn on the thread
1021
- const { cancellable, readyFn } = opts;
1022
- _debugLog('[AsyncTask#immediateSuspend()] args', { cancellable, readyFn });
1023
-
1024
- const pendingCancelled = this.deliverPendingCancel({ cancellable });
1025
- if (pendingCancelled) { return false; }
1026
-
1027
- const cstate = getOrCreateAsyncState(this.#componentIdx);
1028
-
1029
- // TODO(fix): update this to tick until there is no more action to take.
1030
- setTimeout(() => cstate.tick(), 0);
1031
-
1032
- const taskWait = await cstate.suspendTask({ task: this, readyFn });
1033
- const keepGoing = await taskWait;
1034
- return keepGoing;
1035
- }
1036
-
1037
- deliverPendingCancel(opts) {
1038
- const { cancellable } = opts;
1039
- _debugLog('[AsyncTask#deliverPendingCancel()] args', { cancellable });
1040
-
1041
- if (cancellable && this.#state === AsyncTask.State.PENDING_CANCEL) {
1042
- this.#state = Task.State.CANCEL_DELIVERED;
1043
- return true;
1044
- }
1045
-
1046
- return false;
1047
- }
1048
-
1049
- isCancelled() { return this.cancelled }
1050
-
1051
- cancel() {
1052
- _debugLog('[AsyncTask#cancel()] args', { });
1053
- if (!this.taskState() !== AsyncTask.State.CANCEL_DELIVERED) {
1054
- throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] invalid task state for cancellation`);
1055
- }
1056
- if (this.borrowedHandles.length > 0) { throw new Error('task still has borrow handles'); }
1057
- this.cancelled = true;
1058
- this.onResolve(new Error('cancelled'));
1059
- this.#state = AsyncTask.State.RESOLVED;
1060
- }
1061
-
1062
- onResolve(taskValue) {
1063
- for (const f of this.#onResolveHandlers) {
1064
- try {
1065
- f(taskValue);
1066
- } catch (err) {
1067
- console.error("error during task resolve handler", err);
1068
- throw err;
1678
+ this.#state = AsyncTask.State.RESOLVED;
1679
+
1680
+ switch (results.length) {
1681
+ case 0:
1682
+ this.onResolve(undefined);
1683
+ break;
1684
+ case 1:
1685
+ this.onResolve(results[0]);
1686
+ break;
1687
+ default:
1688
+ _debugLog('[AsyncTask#resolve()] unexpected number of results', {
1689
+ componentIdx: this.#componentIdx,
1690
+ results,
1691
+ taskID: this.#id,
1692
+ subtaskID: this.#parentSubtask?.id(),
1693
+ entryFnName: this.#entryFnName,
1694
+ callbackFnName: this.#callbackFnName,
1695
+ });
1696
+ throw new Error('unexpected number of results');
1069
1697
  }
1070
1698
  }
1071
1699
 
1072
- if (this.#postReturnFn) {
1073
- _debugLog('[AsyncTask#onResolve()] running post return ', {
1700
+ exit() {
1701
+ _debugLog('[AsyncTask#exit()]', {
1074
1702
  componentIdx: this.#componentIdx,
1075
1703
  taskID: this.#id,
1076
1704
  });
1077
- this.#postReturnFn();
1705
+
1706
+ if (this.#exited) { throw new Error("task has already exited"); }
1707
+
1708
+ if (this.#state !== AsyncTask.State.RESOLVED) {
1709
+ // TODO(fix): only fused, manually specified post returns seem to break this invariant,
1710
+ // as the TaskReturn trampoline is not activated it seems.
1711
+ //
1712
+ // see: test/p3/ported/wasmtime/component-async/post-return.js
1713
+ //
1714
+ // We *should* be able to upgrade this to be more strict and throw at some point,
1715
+ // which may involve rewriting the upstream test to surface task return manually somehow.
1716
+ //
1717
+ //throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] exited without resolution`);
1718
+ _debugLog('[AsyncTask#exit()] task exited without resolution', {
1719
+ componentIdx: this.#componentIdx,
1720
+ taskID: this.#id,
1721
+ subtask: this.getParentSubtask(),
1722
+ subtaskID: this.getParentSubtask()?.id(),
1723
+ });
1724
+ this.#state = AsyncTask.State.RESOLVED;
1725
+ }
1726
+
1727
+ if (this.borrowedHandles > 0) {
1728
+ throw new Error('task [${this.#id}] exited without clearing borrowed handles');
1729
+ }
1730
+
1731
+ const state = getOrCreateAsyncState(this.#componentIdx);
1732
+ if (!state) { throw new Error('missing async state for component [' + this.#componentIdx + ']'); }
1733
+
1734
+ // Exempt the host from exclusive lock check
1735
+ if (this.#componentIdx !== -1 && this.needsExclusiveLock() && !state.isExclusivelyLocked()) {
1736
+ throw new Error(`task [${this.#id}] exit: component [${this.#componentIdx}] should have been exclusively locked`);
1737
+ }
1738
+
1739
+ state.exclusiveRelease();
1740
+
1741
+ for (const f of this.#onExitHandlers) {
1742
+ try {
1743
+ f();
1744
+ } catch (err) {
1745
+ console.error("error during task exit handler", err);
1746
+ throw err;
1747
+ }
1748
+ }
1749
+
1750
+ this.#exited = true;
1751
+ clearCurrentTask(this.#componentIdx, this.id());
1078
1752
  }
1079
- }
1080
-
1081
- registerOnResolveHandler(f) {
1082
- this.#onResolveHandlers.push(f);
1083
- }
1084
-
1085
- resolve(results) {
1086
- _debugLog('[AsyncTask#resolve()] args', {
1087
- results,
1088
- componentIdx: this.#componentIdx,
1089
- taskID: this.#id,
1090
- });
1091
1753
 
1092
- if (this.#state === AsyncTask.State.RESOLVED) {
1093
- throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] is already resolved (did you forget to wait for an import?)`);
1754
+ needsExclusiveLock() {
1755
+ return !this.#isAsync || this.hasCallback();
1094
1756
  }
1095
- if (this.borrowedHandles.length > 0) { throw new Error('task still has borrow handles'); }
1096
- switch (results.length) {
1097
- case 0:
1098
- this.onResolve(undefined);
1099
- break;
1100
- case 1:
1101
- this.onResolve(results[0]);
1102
- break;
1103
- default:
1104
- throw new Error('unexpected number of results');
1105
- }
1106
- this.#state = AsyncTask.State.RESOLVED;
1107
- }
1108
-
1109
- exit() {
1110
- _debugLog('[AsyncTask#exit()] args', { });
1111
1757
 
1112
- // TODO: ensure there is only one task at a time (scheduler.lock() functionality)
1113
- if (this.#state !== AsyncTask.State.RESOLVED) {
1114
- // TODO(fix): only fused, manually specified post returns seem to break this invariant,
1115
- // as the TaskReturn trampoline is not activated it seems.
1116
- //
1117
- // see: test/p3/ported/wasmtime/component-async/post-return.js
1118
- //
1119
- // We *should* be able to upgrade this to be more strict and throw at some point,
1120
- // which may involve rewriting the upstream test to surface task return manually somehow.
1121
- //
1122
- //throw new Error(`(component [${this.#componentIdx}]) task [${this.#id}] exited without resolution`);
1123
- _debugLog('[AsyncTask#exit()] task exited without resolution', {
1758
+ createSubtask(args) {
1759
+ _debugLog('[AsyncTask#createSubtask()] args', args);
1760
+ const { componentIdx, childTask, callMetadata, fnName, isAsync, isManualAsync } = args;
1761
+
1762
+ const cstate = getOrCreateAsyncState(this.#componentIdx);
1763
+ if (!cstate) {
1764
+ throw new Error(`invalid/missing async state for component idx [${componentIdx}]`);
1765
+ }
1766
+
1767
+ const waitable = new Waitable({
1124
1768
  componentIdx: this.#componentIdx,
1125
- taskID: this.#id,
1126
- subtask: this.getParentSubtask(),
1127
- subtaskID: this.getParentSubtask()?.id(),
1769
+ target: `subtask (internal ID [${this.#id}])`,
1128
1770
  });
1129
- this.#state = AsyncTask.State.RESOLVED;
1771
+
1772
+ const newSubtask = new AsyncSubtask({
1773
+ componentIdx,
1774
+ childTask,
1775
+ parentTask: this,
1776
+ callMetadata,
1777
+ isAsync,
1778
+ isManualAsync,
1779
+ fnName,
1780
+ waitable,
1781
+ });
1782
+ this.#subtasks.push(newSubtask);
1783
+ newSubtask.setTarget(`subtask (internal ID [${newSubtask.id()}], waitable [${waitable.idx()}], component [${componentIdx}])`);
1784
+ waitable.setIdx(cstate.handles.insert(newSubtask));
1785
+ waitable.setTarget(`waitable for subtask (waitable id [${waitable.idx()}], subtask internal ID [${newSubtask.id()}])`);
1786
+
1787
+ return newSubtask;
1130
1788
  }
1131
1789
 
1132
- if (this.borrowedHandles > 0) {
1133
- throw new Error('task [${this.#id}] exited without clearing borrowed handles');
1790
+ getLatestSubtask() {
1791
+ return this.#subtasks.at(-1);
1134
1792
  }
1135
1793
 
1136
- const state = getOrCreateAsyncState(this.#componentIdx);
1137
- if (!state) { throw new Error('missing async state for component [' + this.#componentIdx + ']'); }
1138
- if (!this.#isAsync && !state.inSyncExportCall) {
1139
- throw new Error('sync task must be run from components known to be in a sync export call');
1794
+ getSubtaskByWaitableRep(rep) {
1795
+ if (rep === undefined) { throw new TypeError('missing rep'); }
1796
+ return this.#subtasks.find(s => s.waitableRep() === rep);
1140
1797
  }
1141
- state.inSyncExportCall = false;
1142
1798
 
1143
- if (this.needsExclusiveLock() && !state.isExclusivelyLocked()) {
1144
- throw new Error('task [' + this.#id + '] exit: component [' + this.#componentIdx + '] should have been exclusively locked');
1799
+ currentSubtask() {
1800
+ _debugLog('[AsyncTask#currentSubtask()]');
1801
+ if (this.#subtasks.length === 0) { return undefined; }
1802
+ return this.#subtasks.at(-1);
1145
1803
  }
1146
1804
 
1147
- state.exclusiveRelease();
1148
- }
1149
-
1150
- needsExclusiveLock() { return this.#needsExclusiveLock; }
1151
-
1152
- createSubtask(args) {
1153
- _debugLog('[AsyncTask#createSubtask()] args', args);
1154
- const { componentIdx, childTask, callMetadata } = args;
1155
- const newSubtask = new AsyncSubtask({
1156
- componentIdx,
1157
- childTask,
1158
- parentTask: this,
1159
- callMetadata,
1160
- });
1161
- this.#subtasks.push(newSubtask);
1162
- return newSubtask;
1163
- }
1164
-
1165
- getLatestSubtask() { return this.#subtasks.at(-1); }
1166
-
1167
- currentSubtask() {
1168
- _debugLog('[AsyncTask#currentSubtask()]');
1169
- if (this.#subtasks.length === 0) { return undefined; }
1170
- return this.#subtasks.at(-1);
1805
+ removeSubtask(subtask) {
1806
+ if (this.#subtasks.length === 0) { throw new Error('cannot end current subtask: no current subtask'); }
1807
+ this.#subtasks = this.#subtasks.filter(t => t !== subtask);
1808
+ return subtask;
1809
+ }
1171
1810
  }
1172
1811
 
1173
- endCurrentSubtask() {
1174
- _debugLog('[AsyncTask#endCurrentSubtask()]');
1175
- if (this.#subtasks.length === 0) { throw new Error('cannot end current subtask: no current subtask'); }
1176
- const subtask = this.#subtasks.pop();
1177
- subtask.drop();
1178
- return subtask;
1179
- }
1180
- }
1812
+ const STREAMS = new RepTable({ target: 'global stream map' });
1181
1813
  const ASYNC_STATE = new Map();
1182
1814
 
1183
1815
  function getOrCreateAsyncState(componentIdx, init) {
@@ -1198,7 +1830,6 @@ class ComponentAsyncState {
1198
1830
  #parkedTasks = new Map();
1199
1831
  #suspendedTasksByTaskID = new Map();
1200
1832
  #suspendedTaskIDs = [];
1201
- #pendingTasks = [];
1202
1833
  #errored = null;
1203
1834
 
1204
1835
  #backpressure = 0;
@@ -1207,24 +1838,23 @@ class ComponentAsyncState {
1207
1838
  #handlerMap = new Map();
1208
1839
  #nextHandlerID = 0n;
1209
1840
 
1210
- mayLeave = true;
1841
+ #tickLoop = null;
1842
+ #tickLoopInterval = null;
1843
+
1844
+ #onExclusiveReleaseHandlers = [];
1211
1845
 
1212
- #streams;
1846
+ mayLeave = true;
1213
1847
 
1214
- waitableSets;
1215
- waitables;
1848
+ handles;
1216
1849
  subtasks;
1217
1850
 
1218
1851
  constructor(args) {
1219
1852
  this.#componentIdx = args.componentIdx;
1220
- this.waitableSets = new RepTable({ target: `component [${this.#componentIdx}] waitable sets` });
1221
- this.waitables = new RepTable({ target: `component [${this.#componentIdx}] waitables` });
1853
+ this.handles = new RepTable({ target: `component [${this.#componentIdx}] handles (waitable objects)` });
1222
1854
  this.subtasks = new RepTable({ target: `component [${this.#componentIdx}] subtasks` });
1223
- this.#streams = new Map();
1224
1855
  };
1225
1856
 
1226
1857
  componentIdx() { return this.#componentIdx; }
1227
- streams() { return this.#streams; }
1228
1858
 
1229
1859
  errored() { return this.#errored !== null; }
1230
1860
  setErrored(err) {
@@ -1257,15 +1887,34 @@ class ComponentAsyncState {
1257
1887
  await this.#syncImportWait.promise;
1258
1888
  }
1259
1889
 
1260
- setBackpressure(v) { this.#backpressure = v; }
1261
- getBackpressure(v) { return this.#backpressure; }
1890
+ setBackpressure(v) {
1891
+ this.#backpressure = v;
1892
+ return this.#backpressure
1893
+ }
1894
+ getBackpressure() { return this.#backpressure; }
1895
+
1262
1896
  incrementBackpressure() {
1897
+ const current = this.#backpressure;
1898
+ if (current < 0 || current > 2**16) {
1899
+ throw new Error(`invalid current backpressure value [${current}]`);
1900
+ }
1263
1901
  const newValue = this.getBackpressure() + 1;
1264
- if (newValue > 2**16) { throw new Error("invalid backpressure value, overflow"); }
1265
- this.setBackpressure(newValue);
1902
+ if (newValue >= 2**16) {
1903
+ throw new Error(`invalid new backpressure value [${newValue}], overflow`);
1904
+ }
1905
+ return this.setBackpressure(newValue);
1266
1906
  }
1907
+
1267
1908
  decrementBackpressure() {
1268
- this.setBackpressure(Math.max(0, this.getBackpressure() - 1));
1909
+ const current = this.#backpressure;
1910
+ if (current < 0 || current > 2**16) {
1911
+ throw new Error(`invalid current backpressure value [${current}]`);
1912
+ }
1913
+ const newValue = Math.max(0, current - 1);
1914
+ if (newValue < 0) {
1915
+ throw new Error(`invalid new backpressure value [${newValue}], underflow`);
1916
+ }
1917
+ return this.setBackpressure(newValue);
1269
1918
  }
1270
1919
  hasBackpressure() { return this.#backpressure > 0; }
1271
1920
 
@@ -1330,55 +1979,44 @@ class ComponentAsyncState {
1330
1979
  }
1331
1980
  }
1332
1981
 
1333
- parkTaskOnAwaitable(args) {
1334
- if (!args.awaitable) { throw new TypeError('missing awaitable when trying to park'); }
1335
- if (!args.task) { throw new TypeError('missing task when trying to park'); }
1336
- const { awaitable, task } = args;
1337
-
1338
- let taskList = this.#parkedTasks.get(awaitable.id());
1339
- if (!taskList) {
1340
- taskList = [];
1341
- this.#parkedTasks.set(awaitable.id(), taskList);
1342
- }
1343
- taskList.push(task);
1344
-
1345
- this.wakeNextTaskForAwaitable(awaitable);
1346
- }
1347
-
1348
- wakeNextTaskForAwaitable(awaitable) {
1349
- if (!awaitable) { throw new TypeError('missing awaitable when waking next task'); }
1350
- const awaitableID = awaitable.id();
1351
-
1352
- const taskList = this.#parkedTasks.get(awaitableID);
1353
- if (!taskList || taskList.length === 0) {
1354
- _debugLog('[ComponentAsyncState] no tasks waiting for awaitable', { awaitableID: awaitable.id() });
1355
- return;
1356
- }
1357
-
1358
- let task = taskList.shift(); // todo(perf)
1359
- if (!task) { throw new Error('no task in parked list despite previous check'); }
1360
-
1361
- if (!task.awaitableResume) {
1362
- throw new Error('task ready due to awaitable is missing resume', { taskID: task.id(), awaitableID });
1363
- }
1364
- task.awaitableResume();
1982
+ isExclusivelyLocked() { return this.#locked === true; }
1983
+ setLocked(locked) {
1984
+ this.#locked = locked;
1365
1985
  }
1366
1986
 
1367
- // TODO: we might want to check for pre-locked status here
1987
+ // TODO(fix): we might want to check for pre-locked status here, we should be deterministically
1988
+ // going from locked -> unlocked and vice versa
1368
1989
  exclusiveLock() {
1369
- this.#locked = true;
1990
+ _debugLog('[ComponentAsyncState#exclusiveLock()]', {
1991
+ locked: this.#locked,
1992
+ componentIdx: this.#componentIdx,
1993
+ });
1994
+ this.setLocked(true);
1370
1995
  }
1371
1996
 
1372
1997
  exclusiveRelease() {
1373
- _debugLog('[ComponentAsyncState#exclusiveRelease()] releasing', {
1998
+ _debugLog('[ComponentAsyncState#exclusiveRelease()] args', {
1374
1999
  locked: this.#locked,
1375
2000
  componentIdx: this.#componentIdx,
1376
2001
  });
1377
-
1378
- this.#locked = false
2002
+ this.setLocked(false);
2003
+
2004
+ this.#onExclusiveReleaseHandlers = this.#onExclusiveReleaseHandlers.filter(v => !!v);
2005
+ for (const [idx, f] of this.#onExclusiveReleaseHandlers.entries()) {
2006
+ try {
2007
+ this.#onExclusiveReleaseHandlers[idx] = null;
2008
+ f();
2009
+ } catch (err) {
2010
+ _debugLog("error while executing handler for next exclusive release", err);
2011
+ throw err;
2012
+ }
2013
+ }
1379
2014
  }
1380
2015
 
1381
- isExclusivelyLocked() { return this.#locked === true; }
2016
+ onNextExclusiveRelease(fn) {
2017
+ _debugLog('[ComponentAsyncState#()onNextExclusiveRelease] registering');
2018
+ this.#onExclusiveReleaseHandlers.push(fn);
2019
+ }
1382
2020
 
1383
2021
  #getSuspendedTaskMeta(taskID) {
1384
2022
  return this.#suspendedTasksByTaskID.get(taskID);
@@ -1403,17 +2041,22 @@ class ComponentAsyncState {
1403
2041
  }
1404
2042
  }
1405
2043
 
2044
+ // TODO(threads): readyFn is normally on the thread
1406
2045
  suspendTask(args) {
1407
- // TODO(threads): readyFn is normally on the thread
1408
2046
  const { task, readyFn } = args;
1409
2047
  const taskID = task.id();
1410
- _debugLog('[ComponentAsyncState#suspendTask()]', { taskID });
2048
+ _debugLog('[ComponentAsyncState#suspendTask()]', {
2049
+ taskID,
2050
+ componentIdx: this.#componentIdx,
2051
+ taskEntryFnName: task.entryFnName(),
2052
+ subtask: task.getParentSubtask(),
2053
+ });
1411
2054
 
1412
2055
  if (this.#getSuspendedTaskMeta(taskID)) {
1413
- throw new Error('task [' + taskID + '] already suspended');
2056
+ throw new Error(`task [${taskID}] already suspended`);
1414
2057
  }
1415
2058
 
1416
- const { promise, resolve } = Promise.withResolvers();
2059
+ const { promise, resolve, reject } = promiseWithResolvers();
1417
2060
  this.#addSuspendedTaskMeta({
1418
2061
  task,
1419
2062
  taskID,
@@ -1425,6 +2068,8 @@ class ComponentAsyncState {
1425
2068
  },
1426
2069
  });
1427
2070
 
2071
+ this.runTickLoop();
2072
+
1428
2073
  return promise;
1429
2074
  }
1430
2075
 
@@ -1435,8 +2080,22 @@ class ComponentAsyncState {
1435
2080
  meta.resume();
1436
2081
  }
1437
2082
 
2083
+ async runTickLoop() {
2084
+ if (this.#tickLoop !== null) { return; }
2085
+ this.#tickLoop = 1;
2086
+ setTimeout(async () => {
2087
+ let done = this.tick();
2088
+ while (!done) {
2089
+ await new Promise((resolve) => setTimeout(resolve, 30));
2090
+ done = this.tick();
2091
+ }
2092
+ this.#tickLoop = null;
2093
+ }, 10);
2094
+ }
2095
+
1438
2096
  tick() {
1439
- _debugLog('[ComponentAsyncState#tick()]', { suspendedTaskIDs: this.#suspendedTaskIDs });
2097
+ // _debugLog('[ComponentAsyncState#tick()]', { suspendedTaskIDs: this.#suspendedTaskIDs });
2098
+
1440
2099
  const resumableTasks = this.#suspendedTaskIDs.filter(t => t !== null);
1441
2100
  for (const taskID of resumableTasks) {
1442
2101
  const meta = this.#suspendedTasksByTaskID.get(taskID);
@@ -1444,6 +2103,14 @@ class ComponentAsyncState {
1444
2103
  throw new Error(`missing/invalid task despite ID [${taskID}] being present`);
1445
2104
  }
1446
2105
 
2106
+ // If the task failed via any means, allow the task to resume because
2107
+ // it's been cancelled -- the callback should immediately exit as well
2108
+ if (meta.task.isRejected()) {
2109
+ _debugLog('[ComponentAsyncState#suspendTask()] detected task rejection, leaving early', { meta });
2110
+ this.resumeTaskByID(taskID);
2111
+ return;
2112
+ }
2113
+
1447
2114
  const isReady = meta.readyFn();
1448
2115
  if (!isReady) { continue; }
1449
2116
 
@@ -1453,22 +2120,37 @@ class ComponentAsyncState {
1453
2120
  return this.#suspendedTaskIDs.filter(t => t !== null).length === 0;
1454
2121
  }
1455
2122
 
1456
- addPendingTask(task) {
1457
- this.#pendingTasks.push(task);
1458
- }
1459
-
1460
- addStreamEnd(args) {
2123
+ addStreamEndToTable(args) {
1461
2124
  _debugLog('[ComponentAsyncState#addStreamEnd()] args', args);
1462
2125
  const { tableIdx, streamEnd } = args;
2126
+ if (typeof streamEnd === 'number') { throw new Error("INSERTING BAD STREAMEND"); }
1463
2127
 
1464
- let tbl = this.#streams.get(tableIdx);
1465
- if (!tbl) {
1466
- tbl = new RepTable({ target: `component [${this.#componentIdx}] streams` });
1467
- this.#streams.set(tableIdx, tbl);
2128
+ let { table, componentIdx } = STREAM_TABLES[tableIdx];
2129
+ if (componentIdx === undefined || !table) {
2130
+ throw new Error(`invalid global stream table state for table [${tableIdx}]`);
1468
2131
  }
1469
2132
 
1470
- const streamIdx = tbl.insert(streamEnd);
1471
- return streamIdx;
2133
+ const handle = table.insert(streamEnd);
2134
+ streamEnd.setHandle(handle);
2135
+ streamEnd.setStreamTableIdx(tableIdx);
2136
+
2137
+ const cstate = getOrCreateAsyncState(componentIdx);
2138
+ const waitableIdx = cstate.handles.insert(streamEnd);
2139
+ streamEnd.setWaitableIdx(waitableIdx);
2140
+
2141
+ _debugLog('[ComponentAsyncState#addStreamEnd()] added stream end', {
2142
+ tableIdx,
2143
+ table,
2144
+ handle,
2145
+ streamEnd,
2146
+ destComponentIdx: componentIdx,
2147
+ });
2148
+
2149
+ return { handle, waitableIdx };
2150
+ }
2151
+
2152
+ createWaitable(args) {
2153
+ return new Waitable({ target: args?.target, });
1472
2154
  }
1473
2155
 
1474
2156
  createStream(args) {
@@ -1477,63 +2159,143 @@ class ComponentAsyncState {
1477
2159
  if (tableIdx === undefined) { throw new Error("missing table idx while adding stream"); }
1478
2160
  if (elemMeta === undefined) { throw new Error("missing element metadata while adding stream"); }
1479
2161
 
1480
- let tbl = this.#streams.get(tableIdx);
1481
- if (!tbl) {
1482
- tbl = new RepTable({ target: `component [${this.#componentIdx}] streams` });
1483
- this.#streams.set(tableIdx, tbl);
2162
+ const { table: localStreamTable, componentIdx } = STREAM_TABLES[tableIdx];
2163
+ if (!localStreamTable) {
2164
+ throw new Error(`missing global stream table lookup for table [${tableIdx}] while creating stream`);
1484
2165
  }
2166
+ if (componentIdx !== this.#componentIdx) {
2167
+ throw new Error('component idx mismatch while creating stream');
2168
+ }
2169
+
2170
+ const readWaitable = this.createWaitable();
2171
+ const writeWaitable = this.createWaitable();
1485
2172
 
1486
2173
  const stream = new InternalStream({
1487
2174
  tableIdx,
1488
2175
  componentIdx: this.#componentIdx,
1489
2176
  elemMeta,
2177
+ readWaitable,
2178
+ writeWaitable,
1490
2179
  });
1491
- const writeEndIdx = tbl.insert(stream.getWriteEnd());
1492
- stream.setWriteEndIdx(writeEndIdx);
1493
- const readEndIdx = tbl.insert(stream.getReadEnd());
1494
- stream.setReadEndIdx(readEndIdx);
2180
+ stream.setGlobalStreamMapRep(STREAMS.insert(stream));
2181
+
2182
+ const writeEnd = stream.writeEnd();
2183
+ writeEnd.setWaitableIdx(this.handles.insert(writeEnd));
2184
+ writeEnd.setHandle(localStreamTable.insert(writeEnd));
2185
+ if (writeEnd.streamTableIdx() !== tableIdx) { throw new Error("unexpectedly mismatched stream table"); }
2186
+
2187
+ const writeEndWaitableIdx = writeEnd.waitableIdx();
2188
+ const writeEndHandle = writeEnd.handle();
2189
+ writeWaitable.setTarget(`waitable for stream write end (waitable [${writeEndWaitableIdx}])`);
2190
+ writeEnd.setTarget(`stream write end (waitable [${writeEndWaitableIdx}])`);
1495
2191
 
1496
- const rep = STREAMS.insert(stream);
1497
- stream.setRep(rep);
2192
+ const readEnd = stream.readEnd();
2193
+ readEnd.setWaitableIdx(this.handles.insert(readEnd));
2194
+ readEnd.setHandle(localStreamTable.insert(readEnd));
2195
+ if (readEnd.streamTableIdx() !== tableIdx) { throw new Error("unexpectedly mismatched stream table"); }
1498
2196
 
1499
- return { writeEndIdx, readEndIdx };
2197
+ const readEndWaitableIdx = readEnd.waitableIdx();
2198
+ const readEndHandle = readEnd.handle();
2199
+ readWaitable.setTarget(`waitable for read end (waitable [${readEndWaitableIdx}])`);
2200
+ readEnd.setTarget(`stream read end (waitable [${readEndWaitableIdx}])`);
2201
+
2202
+ return {
2203
+ writeEndWaitableIdx,
2204
+ writeEndHandle,
2205
+ readEndWaitableIdx,
2206
+ readEndHandle,
2207
+ };
1500
2208
  }
1501
2209
 
1502
2210
  getStreamEnd(args) {
1503
2211
  _debugLog('[ComponentAsyncState#getStreamEnd()] args', args);
1504
- const { tableIdx, streamIdx } = args;
1505
- if (tableIdx === undefined) { throw new Error('missing table idx while retrieveing stream end'); }
1506
- if (streamIdx === undefined) { throw new Error('missing stream idx while retrieveing stream end'); }
2212
+ const { tableIdx, streamEndHandle, streamEndWaitableIdx } = args;
2213
+ if (tableIdx === undefined) { throw new Error('missing table idx while getting stream end'); }
2214
+
2215
+ const { table, componentIdx } = STREAM_TABLES[tableIdx];
2216
+ const cstate = getOrCreateAsyncState(componentIdx);
2217
+
2218
+ let streamEnd;
2219
+ if (streamEndWaitableIdx !== undefined) {
2220
+ streamEnd = cstate.handles.get(streamEndWaitableIdx);
2221
+ } else if (streamEndHandle !== undefined) {
2222
+ if (!table) { throw new Error(`missing/invalid table [${tableIdx}] while getting stream end`); }
2223
+ streamEnd = table.get(streamEndHandle);
2224
+ } else {
2225
+ throw new TypeError("must specify either waitable idx or handle to retrieve stream");
2226
+ }
2227
+
2228
+ if (!streamEnd) {
2229
+ throw new Error(`missing stream end (tableIdx [${tableIdx}], handle [${streamEndHandle}], waitableIdx [${streamEndWaitableIdx}])`);
2230
+ }
2231
+ if (tableIdx && streamEnd.streamTableIdx() !== tableIdx) {
2232
+ throw new Error(`stream end table idx [${streamEnd.streamTableIdx()}] does not match [${tableIdx}]`);
2233
+ }
2234
+
2235
+ return streamEnd;
2236
+ }
2237
+
2238
+ deleteStreamEnd(args) {
2239
+ _debugLog('[ComponentAsyncState#deleteStreamEnd()] args', args);
2240
+ const { tableIdx, streamEndWaitableIdx } = args;
2241
+ if (tableIdx === undefined) { throw new Error("missing table idx while removing stream end"); }
2242
+ if (streamEndWaitableIdx === undefined) { throw new Error("missing stream idx while removing stream end"); }
1507
2243
 
1508
- const tbl = this.#streams.get(tableIdx);
1509
- if (!tbl) {
1510
- throw new Error(`missing stream table [${tableIdx}] in component [${this.#componentIdx}] while getting stream`);
2244
+ const { table, componentIdx } = STREAM_TABLES[tableIdx];
2245
+ const cstate = getOrCreateAsyncState(componentIdx);
2246
+
2247
+ const streamEnd = cstate.handles.get(streamEndWaitableIdx);
2248
+ if (!streamEnd) {
2249
+ throw new Error(`missing stream end [${streamEndWaitableIdx}] in component handles while deleting stream`);
2250
+ }
2251
+ if (streamEnd.streamTableIdx() !== tableIdx) {
2252
+ throw new Error(`stream end table idx [${streamEnd.streamTableIdx()}] does not match [${tableIdx}]`);
2253
+ }
2254
+
2255
+ let removed = cstate.handles.remove(streamEnd.waitableIdx());
2256
+ if (!removed) {
2257
+ throw new Error(`failed to remove stream end [${streamEndWaitableIdx}] waitable obj in component [${componentIdx}]`);
2258
+ }
2259
+
2260
+ removed = table.remove(streamEnd.handle());
2261
+ if (!removed) {
2262
+ throw new Error(`failed to remove stream end with handle [${streamEnd.handle()}] from stream table [${tableIdx}] in component [${componentIdx}]`);
1511
2263
  }
1512
2264
 
1513
- const stream = tbl.get(streamIdx);
1514
- return stream;
2265
+ return streamEnd;
1515
2266
  }
1516
2267
 
1517
- removeStreamEnd(args) {
1518
- _debugLog('[ComponentAsyncState#removeStreamEnd()] args', args);
1519
- const { tableIdx, streamIdx } = args;
2268
+ removeStreamEndFromTable(args) {
2269
+ _debugLog('[ComponentAsyncState#removeStreamEndFromTable()] args', args);
2270
+
2271
+ const { tableIdx, streamWaitableIdx } = args;
1520
2272
  if (tableIdx === undefined) { throw new Error("missing table idx while removing stream end"); }
1521
- if (streamIdx === undefined) { throw new Error("missing stream idx while removing stream end"); }
2273
+ if (streamWaitableIdx === undefined) {
2274
+ throw new Error("missing stream end waitable idx while removing stream end");
2275
+ }
2276
+
2277
+ const { table, componentIdx } = STREAM_TABLES[tableIdx];
2278
+ if (!table) { throw new Error(`missing/invalid table [${tableIdx}] while removing stream end`); }
2279
+
2280
+ const cstate = getOrCreateAsyncState(componentIdx);
1522
2281
 
1523
- const tbl = this.#streams.get(tableIdx);
1524
- if (!tbl) {
1525
- throw new Error(`missing stream table [${tableIdx}] in component [${this.#componentIdx}] while removing stream end`);
2282
+ const streamEnd = cstate.handles.get(streamWaitableIdx);
2283
+ if (!streamEnd) {
2284
+ throw new Error(`missing stream end (handle [${streamWaitableIdx}], table [${tableIdx}])`);
1526
2285
  }
2286
+ const handle = streamEnd.handle();
1527
2287
 
1528
- const stream = tbl.get(streamIdx);
1529
- if (!stream) { throw new Error(`component [${this.#componentIdx}] missing stream [${streamIdx}]`); }
2288
+ let removed = cstate.handles.remove(streamWaitableIdx);
2289
+ if (!removed) {
2290
+ throw new Error(`failed to remove streamEnd from handles (waitable idx [${streamWaitableIdx}]), component [${componentIdx}])`);
2291
+ }
1530
2292
 
1531
- const removed = tbl.remove(streamIdx);
2293
+ removed = table.remove(handle);
1532
2294
  if (!removed) {
1533
- throw new Error(`missing stream [${streamIdx}] (table [${tableIdx}]) in component [${this.#componentIdx}] while removing stream end`);
2295
+ throw new Error(`failed to remove streamEnd from table (handle [${handle}]), table [${tableIdx}], component [${componentIdx}])`);
1534
2296
  }
1535
2297
 
1536
- return stream;
2298
+ return streamEnd;
1537
2299
  }
1538
2300
  }
1539
2301
 
@@ -1565,7 +2327,9 @@ const instantiateCore = WebAssembly.instantiate;
1565
2327
  let exports0;
1566
2328
  let memory0;
1567
2329
  let realloc0;
2330
+ let realloc0Async;
1568
2331
  let postReturn0;
2332
+ let postReturn0Async;
1569
2333
  let exports0ParseConfig;
1570
2334
 
1571
2335
  function parseConfig(arg0, arg1) {
@@ -1598,6 +2362,7 @@ function parseConfig(arg0, arg1) {
1598
2362
  const [task, _wasm_call_currentTaskID] = createNewCurrentTask({
1599
2363
  componentIdx: 0,
1600
2364
  isAsync: false,
2365
+ isManualAsync: false,
1601
2366
  entryFnName: 'exports0ParseConfig',
1602
2367
  getCallbackFn: () => null,
1603
2368
  callbackFnName: 'null',
@@ -1605,8 +2370,15 @@ function parseConfig(arg0, arg1) {
1605
2370
  callingWasmExport: true,
1606
2371
  });
1607
2372
 
1608
- let ret = exports0ParseConfig(ptr0, len0, result2, len2);
1609
- endCurrentTask(0);
2373
+ const started = task.enterSync();
2374
+ task.setReturnMemoryIdx(0);
2375
+ task.setReturnMemory(memory0);
2376
+ let ret = _withGlobalCurrentTaskMeta({
2377
+ taskID: task.id(),
2378
+ componentIdx: task.componentIdx(),
2379
+ fn: () => exports0ParseConfig(ptr0, len0, result2, len2),
2380
+ });
2381
+
1610
2382
  let variant60;
1611
2383
  switch (dataView(memory0).getUint8(ret + 0, true)) {
1612
2384
  case 0: {
@@ -2124,11 +2896,13 @@ function parseConfig(arg0, arg1) {
2124
2896
  postReturn: true
2125
2897
  });
2126
2898
  const retCopy = variant60;
2899
+ task.resolve([retCopy.val]);
2127
2900
 
2128
2901
  let cstate = getOrCreateAsyncState(0);
2129
2902
  cstate.mayLeave = false;
2130
2903
  postReturn0(ret);
2131
2904
  cstate.mayLeave = true;
2905
+ task.exit();
2132
2906
 
2133
2907
 
2134
2908
 
@@ -2144,9 +2918,22 @@ const $init = (() => {
2144
2918
  const module0 = fetchCompile(new URL('./parser.core.wasm', import.meta.url));
2145
2919
  ({ exports: exports0 } = yield instantiateCore(yield module0));
2146
2920
  memory0 = exports0.memory;
2147
- GlobalComponentMemories.save({ idx: 0, componentIdx: 0, memory: memory0 });
2148
2921
  realloc0 = exports0.cabi_realloc;
2922
+
2923
+ try {
2924
+ realloc0Async = WebAssembly.promising(exports0.cabi_realloc);
2925
+ } catch(err) {
2926
+ realloc0Async = exports0.cabi_realloc;
2927
+ }
2928
+
2149
2929
  postReturn0 = exports0['cabi_post_parse-config'];
2930
+
2931
+ try {
2932
+ postReturn0Async = WebAssembly.promising(exports0['cabi_post_parse-config']);
2933
+ } catch(err) {
2934
+ postReturn0Async = exports0['cabi_post_parse-config'];
2935
+ }
2936
+
2150
2937
  exports0ParseConfig = exports0['parse-config'];
2151
2938
  })();
2152
2939
  let promise, resolve, reject;