@vzn/vx-dte 0.0.650 → 0.0.652
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -31
- package/package.json +3 -3
- package/src/dispatch.ts +501 -26
- package/src/forecast.ts +1 -2
- package/src/github.ts +16 -5
- package/src/messages.ts +52 -31
- package/src/orchestrator.ts +551 -352
- package/src/planner.ts +1322 -172
- package/src/plugin.ts +48 -8
- package/src/seal.ts +11 -4
- package/src/wire-host.ts +98 -0
- package/src/wire-thread.ts +168 -0
- package/src/wire.ts +117 -68
- package/src/worker.ts +112 -62
package/src/plugin.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type { Need } from './dispatch.js'
|
|
|
20
20
|
import { booked, DEFAULT_STRATEGY, STRATEGIES, type Strategy } from './strategy.js'
|
|
21
21
|
import type { Orchestrator } from './orchestrator.js'
|
|
22
22
|
import type { Proof } from './seal.js'
|
|
23
|
-
import type { Size } from './planner.js'
|
|
23
|
+
import type { Forecast, Size } from './planner.js'
|
|
24
24
|
|
|
25
25
|
/** One kind of worker the adapter can start. */
|
|
26
26
|
export interface WorkerSize {
|
|
@@ -76,6 +76,8 @@ export interface DteAdapter {
|
|
|
76
76
|
* on joining, stuck or queued, so stopped whatever its job holds.
|
|
77
77
|
*/
|
|
78
78
|
cancel?(ids: readonly string[], late?: boolean): void | Promise<readonly string[]>
|
|
79
|
+
/** Worker `id`, given up as late, joined after all and the run took it: a stop not yet sent is dropped. */
|
|
80
|
+
kept?(id: string): void
|
|
79
81
|
/** The CI files that start its workers, which `vx dte init` writes; none for an adapter that starts them on this machine. */
|
|
80
82
|
init?(setup: DteSetup): Promise<DteInit>
|
|
81
83
|
/** Where it says a fault it works around (a status poll or a cancel that failed); called as the run starts. */
|
|
@@ -241,7 +243,13 @@ export function dte(options: DteOptions = {}) {
|
|
|
241
243
|
// With no scheme the run failed in the executor as a bare "Invalid URL".
|
|
242
244
|
if (options.relay !== undefined && !/^https?:\/\//.test(options.relay)) {
|
|
243
245
|
throw new UserError(
|
|
244
|
-
`dte() option "relay" must be an http(s) URL, got ${JSON.stringify(options.relay)}`,
|
|
246
|
+
`dte() option "relay" must be an http(s) URL, got ${JSON.stringify(shown(options.relay))}`,
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
const unrouted = options.relay === undefined ? undefined : unroutable(options.relay)
|
|
250
|
+
if (unrouted !== undefined) {
|
|
251
|
+
throw new UserError(
|
|
252
|
+
`dte() option "relay" ${unrouted}, got ${JSON.stringify(shown(options.relay!))}`,
|
|
245
253
|
)
|
|
246
254
|
}
|
|
247
255
|
// On GitHub Actions with none given, one for the relay's URL.
|
|
@@ -385,7 +393,10 @@ export function dte(options: DteOptions = {}) {
|
|
|
385
393
|
throw new UserError(`vx dte worker: needs ${missing.join(', and ')}`)
|
|
386
394
|
}
|
|
387
395
|
if (!/^https?:\/\//.test(relay))
|
|
388
|
-
throw new UserError(`vx dte worker: VX_DTE_RELAY must be http(s), got ${relay}`)
|
|
396
|
+
throw new UserError(`vx dte worker: VX_DTE_RELAY must be http(s), got ${shown(relay)}`)
|
|
397
|
+
const unroutedEnv = unroutable(relay)
|
|
398
|
+
if (unroutedEnv !== undefined)
|
|
399
|
+
throw new UserError(`vx dte worker: VX_DTE_RELAY ${unroutedEnv}, got ${shown(relay)}`)
|
|
389
400
|
const secret = givenSecret()
|
|
390
401
|
if (secret === undefined && !onGitHub()) {
|
|
391
402
|
throw new UserError(
|
|
@@ -485,13 +496,13 @@ export function dte(options: DteOptions = {}) {
|
|
|
485
496
|
throw new UserError('dte(): no relay: set relay, or use an adapter that has one')
|
|
486
497
|
const [
|
|
487
498
|
{ Orchestrator },
|
|
488
|
-
{
|
|
499
|
+
{ joinOffLoop },
|
|
489
500
|
{ decide, knownMisses, slowdown, tailsMs, WARM_UP_MS },
|
|
490
501
|
{ snapshot },
|
|
491
502
|
seal,
|
|
492
503
|
] = await Promise.all([
|
|
493
504
|
import('./orchestrator.js'),
|
|
494
|
-
import('./wire.js'),
|
|
505
|
+
import('./wire-thread.js'),
|
|
495
506
|
import('./planner.js'),
|
|
496
507
|
import('./tree.js'),
|
|
497
508
|
import('./seal.js'),
|
|
@@ -521,7 +532,9 @@ export function dte(options: DteOptions = {}) {
|
|
|
521
532
|
let onChange = (_now?: boolean): void => {}
|
|
522
533
|
const o = new Orchestrator({
|
|
523
534
|
me: seal.sealOrchestrator(
|
|
524
|
-
|
|
535
|
+
// Off this loop: the run's own sync work held its acks, and every
|
|
536
|
+
// worker then running ended (CI, 2026-10-29).
|
|
537
|
+
await joinOffLoop(relay, run, null, () => token(relay), headers),
|
|
525
538
|
run,
|
|
526
539
|
proof,
|
|
527
540
|
ctx.warn,
|
|
@@ -550,6 +563,7 @@ export function dte(options: DteOptions = {}) {
|
|
|
550
563
|
joined: () => onChange(),
|
|
551
564
|
// A worker given up on joining: its job, still queued or stuck, is stopped.
|
|
552
565
|
overdue: (id) => void adapter?.cancel?.([id], true),
|
|
566
|
+
kept: (id) => adapter?.kept?.(id),
|
|
553
567
|
// Before the run counts what is left to start: a worker that let a
|
|
554
568
|
// task's lease end failed main's macOS run with another to start
|
|
555
569
|
// (2026-10-23).
|
|
@@ -620,6 +634,8 @@ export function dte(options: DteOptions = {}) {
|
|
|
620
634
|
// The run's deadline (`Plan.boundMs`), and the work its plans knew.
|
|
621
635
|
let deadline: number | undefined
|
|
622
636
|
const known = new Set<string>()
|
|
637
|
+
// The last plan, which stands while what it saw holds (`decide`).
|
|
638
|
+
const last: { forecast?: Forecast } = {}
|
|
623
639
|
// A plan that needs a worker later plans again then.
|
|
624
640
|
let again: ReturnType<typeof setTimeout> | undefined
|
|
625
641
|
// The fewest workers that end the run's known work within a margin of
|
|
@@ -662,6 +678,8 @@ export function dte(options: DteOptions = {}) {
|
|
|
662
678
|
how,
|
|
663
679
|
home,
|
|
664
680
|
queued: o.queued(warmUp),
|
|
681
|
+
last,
|
|
682
|
+
now: performance.now(),
|
|
665
683
|
})
|
|
666
684
|
deadline = performance.now() + p.boundMs
|
|
667
685
|
for (const t of p.tasks) known.add(t.id)
|
|
@@ -782,6 +800,8 @@ export function dte(options: DteOptions = {}) {
|
|
|
782
800
|
}
|
|
783
801
|
if (needsIn === undefined) scheduled = forecast
|
|
784
802
|
else forecast()
|
|
803
|
+
// Core asks the footer and the summary after teardown: the last run's graph.
|
|
804
|
+
const depsOf = (id: string): readonly string[] => (nodes ?? ran)?.get(id)?.deps ?? []
|
|
785
805
|
return {
|
|
786
806
|
name: 'dte',
|
|
787
807
|
remote: true,
|
|
@@ -813,9 +833,14 @@ export function dte(options: DteOptions = {}) {
|
|
|
813
833
|
replan()
|
|
814
834
|
return o.execute(req)
|
|
815
835
|
},
|
|
816
|
-
footer: () =>
|
|
836
|
+
footer: () =>
|
|
837
|
+
o.footer(
|
|
838
|
+
() => o.idealMs(depsOf, sizes),
|
|
839
|
+
() => o.advice(depsOf, sizes),
|
|
840
|
+
),
|
|
817
841
|
machines: () => o.machines(),
|
|
818
|
-
idealMs: () => o.idealMs(
|
|
842
|
+
idealMs: () => o.idealMs(depsOf, sizes),
|
|
843
|
+
advice: () => o.advice(depsOf, sizes),
|
|
819
844
|
profile: () => o.profile(),
|
|
820
845
|
running: (id) => o.where(id),
|
|
821
846
|
demand(remaining, misses) {
|
|
@@ -1016,3 +1041,18 @@ export function homeOf(sizes: readonly Size[], host: string): string | undefined
|
|
|
1016
1041
|
const of = new Set(sizes.filter((s) => s.max > 0).map((s) => s.platform))
|
|
1017
1042
|
return of.has(host) && of.size > 1 ? host : undefined
|
|
1018
1043
|
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Why a relay url no route can be appended to is refused: past a `?` or
|
|
1047
|
+
* `#` every socket asked the relay's root, and one naming /v1 asked /v1/v1/….
|
|
1048
|
+
*/
|
|
1049
|
+
function unroutable(url: string): string | undefined {
|
|
1050
|
+
if (/[?#]/.test(url)) return 'takes no ?query or #fragment: each route is appended to it'
|
|
1051
|
+
if (/\/v1\/*$/.test(url)) return "is the relay's base, without /v1"
|
|
1052
|
+
return undefined
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** A relay URL as a refusal may print it: a `user:pass@` would reach the CI log. */
|
|
1056
|
+
function shown(url: string): string {
|
|
1057
|
+
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/i, '$1***@')
|
|
1058
|
+
}
|
package/src/seal.ts
CHANGED
|
@@ -40,17 +40,22 @@ const GITHUB = 'https://token.actions.githubusercontent.com'
|
|
|
40
40
|
/** GitHub's keys are read again for a key not among them at most this often. */
|
|
41
41
|
const KEYS_REREAD_MS = 60_000
|
|
42
42
|
|
|
43
|
+
/** One try at GitHub's token endpoint, which answers in well under a second. */
|
|
44
|
+
const TOKEN_TRY_MS = 3_000
|
|
45
|
+
|
|
43
46
|
/**
|
|
44
|
-
* An OIDC token for `audience` from GitHub's token endpoint. A 5xx
|
|
45
|
-
* dropped connection
|
|
46
|
-
* ended a worker before it joined (PR #4146,
|
|
47
|
+
* An OIDC token for `audience` from GitHub's token endpoint. A 5xx, a
|
|
48
|
+
* dropped connection or a try that hangs past 3 s is tried again, three
|
|
49
|
+
* tries in 10 s at most: one 503 ended a worker before it joined (PR #4146,
|
|
50
|
+
* 2026-10-23), and one held request spent the whole budget (PR #4887,
|
|
51
|
+
* 2026-10-31).
|
|
47
52
|
*/
|
|
48
53
|
export async function oidcToken(url: string, bearer: string, audience: string): Promise<string> {
|
|
49
54
|
const until = performance.now() + 10_000
|
|
50
55
|
for (let attempt = 1; ; attempt++) {
|
|
51
56
|
const res = await fetch(`${url}&audience=${encodeURIComponent(audience)}`, {
|
|
52
57
|
headers: { authorization: `bearer ${bearer}` },
|
|
53
|
-
signal: AbortSignal.timeout(Math.max(1, until - performance.now())),
|
|
58
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(TOKEN_TRY_MS, until - performance.now()))),
|
|
54
59
|
}).catch((err: unknown) => (err instanceof Error ? err.message : String(err)))
|
|
55
60
|
if (typeof res !== 'string' && res.ok) {
|
|
56
61
|
// quiet: a body that is not JSON holds no token, said below
|
|
@@ -371,6 +376,7 @@ export function sealOrchestrator(
|
|
|
371
376
|
return me.online
|
|
372
377
|
},
|
|
373
378
|
resumed: (f) => me.resumed(f),
|
|
379
|
+
quiet: (f) => me.quiet(f),
|
|
374
380
|
async send(to, body) {
|
|
375
381
|
const s = sessions.get(to)
|
|
376
382
|
// Lost or never admitted: the relay's own answer for a peer it does
|
|
@@ -468,6 +474,7 @@ export async function sealWorker(
|
|
|
468
474
|
return me.online
|
|
469
475
|
},
|
|
470
476
|
resumed: (f) => me.resumed(f),
|
|
477
|
+
quiet: (f) => me.quiet(f),
|
|
471
478
|
async send(to, body) {
|
|
472
479
|
if (session === undefined) throw new Error('no session with the orchestrator yet')
|
|
473
480
|
await session.send(body, (sealed) => me.send(to, sealed))
|
package/src/wire-host.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The thread `joinOffLoop` (wire-thread.ts) starts: the orchestrator's
|
|
2
|
+
// participant, its socket, acks, beats and redials, on a loop the run's
|
|
3
|
+
// work never holds.
|
|
4
|
+
|
|
5
|
+
import { join, type Participant } from './wire.js'
|
|
6
|
+
|
|
7
|
+
declare const self: Worker
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The run's loop held this many heartbeats (10 s on the default) is taken
|
|
11
|
+
* as hung: the thread leaves, so the relay closes the run and its workers
|
|
12
|
+
* end, as they would have for an orchestrator quiet that long (owner,
|
|
13
|
+
* 2026-10-23: "if it does not reply for 10s its dead").
|
|
14
|
+
*/
|
|
15
|
+
const HELD_BEATS = 5
|
|
16
|
+
|
|
17
|
+
let me: Participant | undefined
|
|
18
|
+
let n = 0
|
|
19
|
+
const tokens = new Map<number, { resolve(v: string | undefined): void; reject(e: Error): void }>()
|
|
20
|
+
const post = (m: unknown): void => self.postMessage(m)
|
|
21
|
+
const failure = (err: unknown): { status?: number; message: string } => ({
|
|
22
|
+
...(typeof (err as { status?: unknown }).status === 'number'
|
|
23
|
+
? { status: (err as { status: number }).status }
|
|
24
|
+
: {}),
|
|
25
|
+
message: err instanceof Error ? err.message : String(err),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
self.onmessage = async (e: MessageEvent<Record<string, unknown>>) => {
|
|
29
|
+
const m = e.data
|
|
30
|
+
if (m['type'] === 'token') {
|
|
31
|
+
const t = tokens.get(m['n'] as number)
|
|
32
|
+
tokens.delete(m['n'] as number)
|
|
33
|
+
if (typeof m['error'] === 'string') t?.reject(new Error(m['error']))
|
|
34
|
+
else t?.resolve(m['value'] as string | undefined)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const reply = (value?: unknown): void => post({ type: 'done', n: m['n'], value })
|
|
38
|
+
const fail = (err: unknown): void => post({ type: 'done', n: m['n'], error: failure(err) })
|
|
39
|
+
try {
|
|
40
|
+
if (m['type'] === 'join') {
|
|
41
|
+
const shared = new Float64Array(m['shared'] as SharedArrayBuffer)
|
|
42
|
+
const token = m['asks']
|
|
43
|
+
? () =>
|
|
44
|
+
new Promise<string | undefined>((resolve, reject) => {
|
|
45
|
+
const k = ++n
|
|
46
|
+
tokens.set(k, { resolve, reject })
|
|
47
|
+
post({ type: 'token', n: k })
|
|
48
|
+
})
|
|
49
|
+
: (m['token'] as string | undefined)
|
|
50
|
+
const p = await join(
|
|
51
|
+
m['relay'] as string,
|
|
52
|
+
m['run'] as string,
|
|
53
|
+
'orchestrator',
|
|
54
|
+
m['info'],
|
|
55
|
+
token,
|
|
56
|
+
m['extra'] as Record<string, string>,
|
|
57
|
+
)
|
|
58
|
+
me = p
|
|
59
|
+
p.resumed((peer) => post({ type: 'resumed', ...(peer === undefined ? {} : { peer }) }))
|
|
60
|
+
reply({ id: p.id, heartbeatMs: p.heartbeatMs })
|
|
61
|
+
const watch = setInterval(
|
|
62
|
+
() => {
|
|
63
|
+
shared[1] = p.online ? 1 : 0
|
|
64
|
+
const held = Date.now() - shared[0]!
|
|
65
|
+
if (held < HELD_BEATS * p.heartbeatMs) return
|
|
66
|
+
clearInterval(watch)
|
|
67
|
+
post({
|
|
68
|
+
type: 'ended',
|
|
69
|
+
status: 410,
|
|
70
|
+
message: `dte: this machine's run was held ${Math.round(held / 1000)} s, past ${HELD_BEATS} heartbeats: the run is over`,
|
|
71
|
+
})
|
|
72
|
+
// quiet: `ended` above already said the run is over; a failed
|
|
73
|
+
// leave changes nothing the relay does with it.
|
|
74
|
+
void p.leave().catch(() => undefined)
|
|
75
|
+
},
|
|
76
|
+
Math.max(10, p.heartbeatMs / 10),
|
|
77
|
+
)
|
|
78
|
+
for (;;) {
|
|
79
|
+
let got
|
|
80
|
+
try {
|
|
81
|
+
got = await p.poll()
|
|
82
|
+
} catch (err) {
|
|
83
|
+
clearInterval(watch)
|
|
84
|
+
const f = failure(err)
|
|
85
|
+
post({ type: 'ended', status: f.status ?? 500, message: f.message })
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
if (got.length > 0) post({ type: 'messages', messages: got })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (me === undefined) throw new Error('dte: not joined')
|
|
92
|
+
if (m['type'] === 'send') reply(await me.send(m['to'] as string, m['body']))
|
|
93
|
+
else if (m['type'] === 'remove') reply(await me.remove(m['worker'] as string))
|
|
94
|
+
else if (m['type'] === 'leave') reply(await me.leave())
|
|
95
|
+
} catch (err) {
|
|
96
|
+
fail(err)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The orchestrator's relay participant on a thread of its own.
|
|
2
|
+
//
|
|
3
|
+
// Liveness is in the protocol: a quiet worker reports every 1.5
|
|
4
|
+
// heartbeats, its orchestrator acks, and a peer quiet 3 heartbeats is lost. On the orchestrator's loop, its own
|
|
5
|
+
// sync work held its beats: in CI run 40886829588 (2026-10-29) it said
|
|
6
|
+
// nothing for 12 s and every worker then running ended, stopping 13
|
|
7
|
+
// tasks. Here the socket, its acks, beats and redials run on a thread the
|
|
8
|
+
// run's work never holds; the run talks to it by messages.
|
|
9
|
+
|
|
10
|
+
import { ui } from '@vzn/vx'
|
|
11
|
+
import { RelayError, type Participant, type RelayMessage } from './wire.js'
|
|
12
|
+
|
|
13
|
+
/** From the thread: what `wire-host.ts` posts. */
|
|
14
|
+
type FromHost =
|
|
15
|
+
| { type: 'token'; n: number }
|
|
16
|
+
| { type: 'messages'; messages: RelayMessage[] }
|
|
17
|
+
| { type: 'resumed'; peer?: string }
|
|
18
|
+
| { type: 'ended'; status: number; message: string }
|
|
19
|
+
| { type: 'done'; n: number; value?: unknown; error?: { status?: number; message: string } }
|
|
20
|
+
|
|
21
|
+
/** How often this loop says it is alive to the thread (`HELD_BEATS` in wire-host.ts reads it). */
|
|
22
|
+
const TICK_MS = 100
|
|
23
|
+
/**
|
|
24
|
+
* A tick this late says so: the thread kept the run alive, but what held the
|
|
25
|
+
* loop is not known (run 40886829588's profile had no slice in its 12 s gap),
|
|
26
|
+
* and the line's time in the log is where to look. Half the 2 s heartbeat.
|
|
27
|
+
*/
|
|
28
|
+
const HELD_SAY_MS = 1000
|
|
29
|
+
|
|
30
|
+
export async function joinOffLoop(
|
|
31
|
+
relay: string,
|
|
32
|
+
run: string,
|
|
33
|
+
info: unknown,
|
|
34
|
+
token: string | undefined | (() => Promise<string | undefined>),
|
|
35
|
+
extra: Readonly<Record<string, string>> = {},
|
|
36
|
+
): Promise<Participant> {
|
|
37
|
+
// [0]: when this loop last ran (Date.now, the clock both threads share); [1]: online.
|
|
38
|
+
const shared = new Float64Array(new SharedArrayBuffer(16))
|
|
39
|
+
const tick = (): void => {
|
|
40
|
+
const now = Date.now()
|
|
41
|
+
const held = now - shared[0]! - TICK_MS
|
|
42
|
+
if (shared[0] && held >= HELD_SAY_MS)
|
|
43
|
+
ui.say('warn', `[vx] dte: this machine's loop was held ${held} ms`)
|
|
44
|
+
shared[0] = now
|
|
45
|
+
}
|
|
46
|
+
tick()
|
|
47
|
+
const ticking = setInterval(tick, TICK_MS)
|
|
48
|
+
ticking.unref()
|
|
49
|
+
const thread = new Worker(new URL('./wire-host.ts', import.meta.url).href)
|
|
50
|
+
let n = 0
|
|
51
|
+
const calls = new Map<number, { resolve(v: unknown): void; reject(e: unknown): void }>()
|
|
52
|
+
const inbox: RelayMessage[] = []
|
|
53
|
+
let ended: RelayError | undefined
|
|
54
|
+
let onResumed: ((peer?: string) => void) | undefined
|
|
55
|
+
/** The token function's own error, said as itself rather than as the thread's copy. */
|
|
56
|
+
let tokenError: unknown
|
|
57
|
+
const waiters = new Set<() => void>()
|
|
58
|
+
const wake = (): void => {
|
|
59
|
+
for (const w of [...waiters]) w()
|
|
60
|
+
}
|
|
61
|
+
const call = (msg: Record<string, unknown>): Promise<unknown> =>
|
|
62
|
+
new Promise((resolve, reject) => {
|
|
63
|
+
const k = ++n
|
|
64
|
+
calls.set(k, { resolve, reject })
|
|
65
|
+
thread.postMessage({ ...msg, n: k })
|
|
66
|
+
})
|
|
67
|
+
thread.onmessage = (e: MessageEvent<FromHost>) => {
|
|
68
|
+
const m = e.data
|
|
69
|
+
if (m.type === 'token') {
|
|
70
|
+
void (async () => {
|
|
71
|
+
try {
|
|
72
|
+
const value = typeof token === 'function' ? await token() : token
|
|
73
|
+
thread.postMessage({ type: 'token', n: m.n, value })
|
|
74
|
+
} catch (err) {
|
|
75
|
+
tokenError = err
|
|
76
|
+
thread.postMessage({
|
|
77
|
+
type: 'token',
|
|
78
|
+
n: m.n,
|
|
79
|
+
error: err instanceof Error ? err.message : String(err),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
})()
|
|
83
|
+
} else if (m.type === 'messages') {
|
|
84
|
+
inbox.push(...m.messages)
|
|
85
|
+
wake()
|
|
86
|
+
} else if (m.type === 'resumed') onResumed?.(m.peer)
|
|
87
|
+
else if (m.type === 'ended') {
|
|
88
|
+
ended ??= new RelayError(m.status, m.message)
|
|
89
|
+
wake()
|
|
90
|
+
} else {
|
|
91
|
+
const c = calls.get(m.n)
|
|
92
|
+
calls.delete(m.n)
|
|
93
|
+
if (m.error === undefined) c?.resolve(m.value)
|
|
94
|
+
else if (tokenError instanceof Error && tokenError.message === m.error.message)
|
|
95
|
+
c?.reject(tokenError)
|
|
96
|
+
else
|
|
97
|
+
c?.reject(
|
|
98
|
+
m.error.status === undefined
|
|
99
|
+
? new Error(m.error.message)
|
|
100
|
+
: new RelayError(m.error.status, m.error.message),
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const stop = (): void => {
|
|
105
|
+
clearInterval(ticking)
|
|
106
|
+
thread.terminate()
|
|
107
|
+
}
|
|
108
|
+
let joined: { id: string; heartbeatMs: number }
|
|
109
|
+
try {
|
|
110
|
+
joined = (await call({
|
|
111
|
+
type: 'join',
|
|
112
|
+
relay,
|
|
113
|
+
run,
|
|
114
|
+
info,
|
|
115
|
+
extra,
|
|
116
|
+
shared: shared.buffer,
|
|
117
|
+
token: typeof token === 'function' ? undefined : token,
|
|
118
|
+
asks: typeof token === 'function',
|
|
119
|
+
})) as { id: string; heartbeatMs: number }
|
|
120
|
+
} catch (err) {
|
|
121
|
+
stop()
|
|
122
|
+
throw err
|
|
123
|
+
}
|
|
124
|
+
const idle = (ms: number, signal?: AbortSignal): Promise<void> =>
|
|
125
|
+
new Promise((resolve) => {
|
|
126
|
+
const done = (): void => {
|
|
127
|
+
clearTimeout(timer)
|
|
128
|
+
signal?.removeEventListener('abort', done)
|
|
129
|
+
waiters.delete(done)
|
|
130
|
+
resolve()
|
|
131
|
+
}
|
|
132
|
+
const timer = setTimeout(done, ms)
|
|
133
|
+
signal?.addEventListener('abort', done)
|
|
134
|
+
waiters.add(done)
|
|
135
|
+
})
|
|
136
|
+
return {
|
|
137
|
+
id: joined.id,
|
|
138
|
+
heartbeatMs: joined.heartbeatMs,
|
|
139
|
+
get online() {
|
|
140
|
+
return shared[1] === 1
|
|
141
|
+
},
|
|
142
|
+
resumed(f) {
|
|
143
|
+
onResumed = f
|
|
144
|
+
},
|
|
145
|
+
// The thread's own wire beats for the run: its loop may be held.
|
|
146
|
+
quiet() {},
|
|
147
|
+
async send(to, body) {
|
|
148
|
+
await call({ type: 'send', to, body })
|
|
149
|
+
},
|
|
150
|
+
async poll(signal) {
|
|
151
|
+
if (inbox.length === 0 && ended === undefined && signal?.aborted !== true)
|
|
152
|
+
await idle(joined.heartbeatMs, signal)
|
|
153
|
+
if (inbox.length > 0) return inbox.splice(0)
|
|
154
|
+
if (ended !== undefined) throw ended
|
|
155
|
+
return []
|
|
156
|
+
},
|
|
157
|
+
async remove(worker) {
|
|
158
|
+
await call({ type: 'remove', worker })
|
|
159
|
+
},
|
|
160
|
+
async leave() {
|
|
161
|
+
try {
|
|
162
|
+
await call({ type: 'leave' })
|
|
163
|
+
} finally {
|
|
164
|
+
stop()
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
}
|