@toa.io/extensions.cadence 1.0.0-alpha.291 → 1.0.0-alpha.293
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/package.json +15 -6
- package/transpiled/Dispatcher.js +5 -2
- package/transpiled/Dispatcher.js.map +1 -1
- package/transpiled/Factory.js +1 -1
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/index.d.ts +2 -5
- package/transpiled/index.js +2 -4
- package/transpiled/index.js.map +1 -1
- package/CHANGELOG.md +0 -37
- package/schemas/annotation.cos.yaml +0 -9
- package/schemas/declaration.cos.yaml +0 -20
- package/source/Aspect.ts +0 -73
- package/source/Composition.ts +0 -47
- package/source/Dispatcher.test.ts +0 -479
- package/source/Dispatcher.ts +0 -334
- package/source/Factory.ts +0 -76
- package/source/Local.ts +0 -40
- package/source/Pulse.test.ts +0 -303
- package/source/Pulse.ts +0 -211
- package/source/const.ts +0 -31
- package/source/context.ts +0 -10
- package/source/deployment.ts +0 -38
- package/source/index.ts +0 -8
- package/source/manifest.test.ts +0 -61
- package/source/manifest.ts +0 -64
- package/source/schemas.ts +0 -10
- package/source/types.ts +0 -41
- package/transpiled/const.d.ts +0 -21
- package/transpiled/const.js +0 -26
- package/transpiled/const.js.map +0 -1
- package/transpiled/context.d.ts +0 -3
- package/transpiled/context.js +0 -9
- package/transpiled/context.js.map +0 -1
- package/transpiled/deployment.d.ts +0 -9
- package/transpiled/deployment.js +0 -29
- package/transpiled/deployment.js.map +0 -1
- package/transpiled/manifest.d.ts +0 -11
- package/transpiled/manifest.js +0 -42
- package/transpiled/manifest.js.map +0 -1
- package/transpiled/schemas.d.ts +0 -4
- package/transpiled/schemas.js +0 -7
- package/transpiled/schemas.js.map +0 -1
- package/transpiled/types.d.ts +0 -34
- package/transpiled/types.js +0 -2
- package/transpiled/types.js.map +0 -1
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
package/source/Dispatcher.ts
DELETED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
import { console } from 'openspan'
|
|
2
|
-
import { Connector, Locator } from '@toa.io/core'
|
|
3
|
-
import { BATCH, DISCRETENESS, LANES, number } from './const.js'
|
|
4
|
-
import type { Local } from './Local.js'
|
|
5
|
-
import type { atomicity } from '@toa.io/core/types'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Makes the calls that were put off.
|
|
9
|
-
*
|
|
10
|
-
* Two tiers, which is what a store polled rarely and a call due to the second need between
|
|
11
|
-
* them: the rows are the record, and one scan primes the next two intervals of them into
|
|
12
|
-
* timers. A crash loses the timers and nothing else — the next scan reads what is overdue
|
|
13
|
-
* along with what is upcoming, so a dispatcher that died mid-interval costs a delay and not a
|
|
14
|
-
* call.
|
|
15
|
-
*
|
|
16
|
-
* Partitioned by lane exactly as the outbox is, so every replica scans and dispatches its own
|
|
17
|
-
* share of every interval rather than taking turns at whole ones.
|
|
18
|
-
*/
|
|
19
|
-
export class Dispatcher extends Connector {
|
|
20
|
-
private readonly metronome: Local
|
|
21
|
-
private readonly resolve: (locator: Locator) => Local
|
|
22
|
-
private readonly atom: atomicity.Atom
|
|
23
|
-
|
|
24
|
-
/** rows in this replica's hands — a timer set for them, or a call of theirs on its way */
|
|
25
|
-
private readonly armed = new Map<string, Armed>()
|
|
26
|
-
|
|
27
|
-
/** ids called and not yet removed, held until a scan settles them */
|
|
28
|
-
private called = new Set<string>()
|
|
29
|
-
|
|
30
|
-
/** what it has called through, so that each is torn down with it */
|
|
31
|
-
private readonly targets = new Set<Local>()
|
|
32
|
-
|
|
33
|
-
/** milliseconds between passes, and how far ahead each one reaches */
|
|
34
|
-
private readonly discreteness: number
|
|
35
|
-
|
|
36
|
-
private timer?: NodeJS.Timeout
|
|
37
|
-
private off?: () => void
|
|
38
|
-
private scanning = false
|
|
39
|
-
private closing = false
|
|
40
|
-
|
|
41
|
-
/** when the scan that is running started, so a tick that finds it can say how long */
|
|
42
|
-
private started = 0
|
|
43
|
-
|
|
44
|
-
public constructor(
|
|
45
|
-
metronome: Local,
|
|
46
|
-
resolve: (locator: Locator) => Local,
|
|
47
|
-
atom: atomicity.Atom
|
|
48
|
-
) {
|
|
49
|
-
super()
|
|
50
|
-
|
|
51
|
-
this.metronome = metronome
|
|
52
|
-
this.resolve = resolve
|
|
53
|
-
this.atom = atom
|
|
54
|
-
this.discreteness = number('TOA_CADENCE_DISCRETENESS', DISCRETENESS * 1000)
|
|
55
|
-
|
|
56
|
-
this.depends(metronome)
|
|
57
|
-
this.depends(atom)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
protected override async open(): Promise<void> {
|
|
61
|
-
this.timer = setInterval(() => {
|
|
62
|
-
this.tick()
|
|
63
|
-
}, this.discreteness)
|
|
64
|
-
this.timer.unref()
|
|
65
|
-
|
|
66
|
-
// a lane changing hands is when its rows become this replica's, or stop being
|
|
67
|
-
this.off = this.atom.onassigned(() => {
|
|
68
|
-
this.adopt()
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
protected override async close(): Promise<void> {
|
|
73
|
-
this.closing = true
|
|
74
|
-
|
|
75
|
-
this.off?.()
|
|
76
|
-
clearInterval(this.timer)
|
|
77
|
-
|
|
78
|
-
for (const { timer } of this.armed.values()) clearTimeout(timer)
|
|
79
|
-
|
|
80
|
-
this.armed.clear()
|
|
81
|
-
|
|
82
|
-
await this.settle()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** One scan at a time: a slow one must not have another started underneath it. */
|
|
86
|
-
private tick(): void {
|
|
87
|
-
if (this.closing) return
|
|
88
|
-
|
|
89
|
-
/*
|
|
90
|
-
* Skipped, and said so. The guard itself is right — a second scan on top of one that has
|
|
91
|
-
* not returned only makes it worse — but a pass that outlives its interval takes every
|
|
92
|
-
* pass after it with it, and nothing is dispatched at all while that lasts. Silently, it
|
|
93
|
-
* is a dispatcher that stops dispatching in a deployment that ran for months.
|
|
94
|
-
*/
|
|
95
|
-
if (this.scanning) {
|
|
96
|
-
const running = Date.now() - this.started
|
|
97
|
-
const details = { running, discreteness: this.discreteness }
|
|
98
|
-
const message = 'Delayed calls scan has not returned, so this pass is skipped'
|
|
99
|
-
|
|
100
|
-
// past a few intervals it is not a slow pass but a stuck one
|
|
101
|
-
if (running > STALLED * this.discreteness) console.error(message, details)
|
|
102
|
-
else console.warn(message, details)
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
this.scanning = true
|
|
108
|
-
this.started = Date.now()
|
|
109
|
-
|
|
110
|
-
void this.scan()
|
|
111
|
-
.catch((error: unknown) => {
|
|
112
|
-
console.error('Delayed calls scan failed', { error })
|
|
113
|
-
})
|
|
114
|
-
.finally(() => {
|
|
115
|
-
this.scanning = false
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* What this replica owns has changed. Timers for lanes it no longer holds are dropped —
|
|
121
|
-
* whoever holds them now will arm them — and a scan runs at once rather than at the next
|
|
122
|
-
* interval, so lanes just acquired are not left waiting.
|
|
123
|
-
*/
|
|
124
|
-
private adopt(): void {
|
|
125
|
-
// nothing owned is nothing held: what was armed under a claim this replica no longer has
|
|
126
|
-
// belongs to whoever has it now, and firing it anyway is the call twice
|
|
127
|
-
this.disarm(this.atom.slots(LANES) ?? [])
|
|
128
|
-
|
|
129
|
-
this.tick()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** What is armed for a lane this replica no longer holds: whoever holds it now will arm it. */
|
|
133
|
-
private disarm(lanes: number[]): void {
|
|
134
|
-
for (const [id, { timer, lane }] of this.armed) {
|
|
135
|
-
if (lanes.includes(lane)) continue
|
|
136
|
-
|
|
137
|
-
clearTimeout(timer)
|
|
138
|
-
this.armed.delete(id)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private async scan(): Promise<void> {
|
|
143
|
-
// ahead of the claim, because settling needs none: the rows are ones this replica has
|
|
144
|
-
// already called, and one left live is one the next owner of its lane calls again
|
|
145
|
-
await this.settle()
|
|
146
|
-
|
|
147
|
-
const lanes = this.atom.slots(LANES)
|
|
148
|
-
|
|
149
|
-
if (lanes === null || lanes.length === 0) return
|
|
150
|
-
|
|
151
|
-
const now = Date.now()
|
|
152
|
-
|
|
153
|
-
/*
|
|
154
|
-
* Two intervals, so that consecutive passes overlap by one whole one. Reaching exactly as
|
|
155
|
-
* far as the next pass leaves a sliver beyond the horizon that the next pass reads already
|
|
156
|
-
* overdue — the width of the drift between the two, which a row with a tight `overdue`
|
|
157
|
-
* does not survive. The overlap absorbs that, a settle that ran long, a tick skipped
|
|
158
|
-
* outright, and the clock skew between the replica that wrote `due` and this one.
|
|
159
|
-
*
|
|
160
|
-
* Reaching further costs nothing a cancellation would pay for, because what is armed is
|
|
161
|
-
* given up again the moment a scan stops reading it — see `forget`.
|
|
162
|
-
*/
|
|
163
|
-
const until = now + 2 * this.discreteness
|
|
164
|
-
|
|
165
|
-
const rows = (await this.metronome.invoke('enumerate', {
|
|
166
|
-
query: {
|
|
167
|
-
// expiry is not a criterion: a row nobody reads is a row nobody settles, and it would
|
|
168
|
-
// stay live for good. It is read, and settled without the call being made
|
|
169
|
-
criteria: `lane=in=(${lanes.join(',')});due<${until}`,
|
|
170
|
-
sort: ['due:asc'],
|
|
171
|
-
limit: BATCH
|
|
172
|
-
}
|
|
173
|
-
})) as Row[]
|
|
174
|
-
|
|
175
|
-
// the horizon is truncated to the last row of a full batch, and everything past it arrives
|
|
176
|
-
// on a later pass already overdue. Where this is logged, the batch is too small for the rate
|
|
177
|
-
const truncated = rows.length === BATCH
|
|
178
|
-
|
|
179
|
-
if (truncated)
|
|
180
|
-
console.warn('Delayed calls scan filled its batch', { limit: BATCH, lanes: lanes.length })
|
|
181
|
-
|
|
182
|
-
for (const row of rows) this.arm(row)
|
|
183
|
-
|
|
184
|
-
if (!truncated) this.forget(new Set(rows.map((row) => row.id)))
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Gives up what the store no longer answers with. A scan reads everything still owed in the
|
|
189
|
-
* lanes this replica holds, so a row it armed before and does not read now is one nothing
|
|
190
|
-
* owes any more — cancelled, most of the time — and a timer left for it makes the call
|
|
191
|
-
* regardless.
|
|
192
|
-
*
|
|
193
|
-
* This is what lets a scan reach further ahead than the next one without a cancellation
|
|
194
|
-
* becoming any less reliable: a row is held only for as long as the store keeps saying it is
|
|
195
|
-
* owed, so what bounds cancelling is the period between passes and not how far one reaches.
|
|
196
|
-
*
|
|
197
|
-
* Two things are not evidence of a cancellation. A batch filled to its limit is a read that
|
|
198
|
-
* was cut short, and says nothing about what it did not reach. And a call already on its way
|
|
199
|
-
* is settled by its own path rather than by what a scan reads.
|
|
200
|
-
*/
|
|
201
|
-
private forget(read: Set<string>): void {
|
|
202
|
-
for (const [id, { timer }] of this.armed) {
|
|
203
|
-
if (read.has(id) || this.called.has(id)) continue
|
|
204
|
-
|
|
205
|
-
clearTimeout(timer)
|
|
206
|
-
this.armed.delete(id)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* A row already armed is left alone: a scan reads what it read before, because a row stays
|
|
212
|
-
* until its call has been made.
|
|
213
|
-
*/
|
|
214
|
-
private arm(row: Row): void {
|
|
215
|
-
if (this.armed.has(row.id) || this.called.has(row.id)) return
|
|
216
|
-
|
|
217
|
-
const now = Date.now()
|
|
218
|
-
|
|
219
|
-
/*
|
|
220
|
-
* Past the bound its caller gave it, so the call is not made — and the row is settled all
|
|
221
|
-
* the same, silently, because an expired call is not an event anything sees. Left unread
|
|
222
|
-
* it would never be settled either: a row nothing will ever act on, sitting in the
|
|
223
|
-
* collection and in front of every scan, for good.
|
|
224
|
-
*/
|
|
225
|
-
if (row.expires <= now) {
|
|
226
|
-
this.called.add(row.id)
|
|
227
|
-
|
|
228
|
-
return
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const timer = setTimeout(
|
|
232
|
-
() => {
|
|
233
|
-
void this.dispatch(row)
|
|
234
|
-
},
|
|
235
|
-
Math.max(row.due - now, 0)
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
timer.unref()
|
|
239
|
-
this.armed.set(row.id, { timer, lane: row.lane })
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* The call goes out before the row is removed, never the other way round: a crash in between
|
|
244
|
-
* makes the call twice, and the other order would lose it. It travels as a task, so the
|
|
245
|
-
* broker holds it if the target is not there to take it.
|
|
246
|
-
*/
|
|
247
|
-
private async dispatch(row: Row): Promise<void> {
|
|
248
|
-
if (this.closing) {
|
|
249
|
-
this.armed.delete(row.id)
|
|
250
|
-
|
|
251
|
-
return
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const [namespace, name, endpoint] = row.endpoint.split('.')
|
|
255
|
-
const local = this.target(new Locator(name, namespace))
|
|
256
|
-
|
|
257
|
-
/*
|
|
258
|
-
* Attempted before it is made, not after it comes back. One attempt is what a dispatcher
|
|
259
|
-
* has to give: what it can tell about a failure is nothing, and a request the target's
|
|
260
|
-
* schema no longer fits never becomes one it does.
|
|
261
|
-
*/
|
|
262
|
-
this.called.add(row.id)
|
|
263
|
-
|
|
264
|
-
/*
|
|
265
|
-
* The call travels as a task, so what raises here is this side of it: a stored request the
|
|
266
|
-
* target's contract no longer fits, an endpoint it no longer has, a broker that refused the
|
|
267
|
-
* enqueue. What the operation itself does with it happens in the target's own process and
|
|
268
|
-
* never comes back.
|
|
269
|
-
*
|
|
270
|
-
* Skipped, as a pulse skips: reported, and the row settled on the attempt either way.
|
|
271
|
-
*/
|
|
272
|
-
await local
|
|
273
|
-
.invoke(endpoint, { input: null, ...row.request, task: true })
|
|
274
|
-
.catch((error: unknown) => {
|
|
275
|
-
console.error('Delayed call failed', {
|
|
276
|
-
endpoint: row.endpoint,
|
|
277
|
-
id: row.id,
|
|
278
|
-
error
|
|
279
|
-
})
|
|
280
|
-
})
|
|
281
|
-
// held for as long as the call is, so `armed` answers what this replica has in hand
|
|
282
|
-
// rather than what it has a timer for
|
|
283
|
-
.finally(() => {
|
|
284
|
-
this.armed.delete(row.id)
|
|
285
|
-
})
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* A remote is resolved once per target and held: it opens a connection, and one nothing
|
|
290
|
-
* depends on would outlive the dispatcher that made it.
|
|
291
|
-
*/
|
|
292
|
-
private target(locator: Locator): Local {
|
|
293
|
-
const local = this.resolve(locator)
|
|
294
|
-
|
|
295
|
-
if (!this.targets.has(local)) {
|
|
296
|
-
this.targets.add(local)
|
|
297
|
-
this.depends(local)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return local
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/** One write for many rows, which is why the ids are held rather than settled one by one. */
|
|
304
|
-
private async settle(): Promise<void> {
|
|
305
|
-
if (this.called.size === 0) return
|
|
306
|
-
|
|
307
|
-
const ids = [...this.called]
|
|
308
|
-
|
|
309
|
-
await this.metronome.invoke('settle', { query: { ids }, input: null })
|
|
310
|
-
|
|
311
|
-
// held until the write comes back, so that one which does not leaves them to a later scan
|
|
312
|
-
for (const id of ids) this.called.delete(id)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
interface Armed {
|
|
317
|
-
timer: NodeJS.Timeout
|
|
318
|
-
lane: number
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
interface Row {
|
|
322
|
-
id: string
|
|
323
|
-
lane: number
|
|
324
|
-
due: number
|
|
325
|
-
|
|
326
|
-
/** past this the call is not made; the row is settled without it */
|
|
327
|
-
expires: number
|
|
328
|
-
|
|
329
|
-
endpoint: string
|
|
330
|
-
request?: object
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/** intervals a scan may run for before it is a stuck pass rather than a slow one */
|
|
334
|
-
const STALLED = 5
|
package/source/Factory.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { Connector, Locator } from '@toa.io/core'
|
|
2
|
-
import { Composition } from './Composition.js'
|
|
3
|
-
import { COMPONENT, NAMESPACE } from './const.js'
|
|
4
|
-
import { Aspect } from './Aspect.js'
|
|
5
|
-
import { Dispatcher } from './Dispatcher.js'
|
|
6
|
-
import { Local } from './Local.js'
|
|
7
|
-
import { Pulse } from './Pulse.js'
|
|
8
|
-
import type { Declaration } from './types.js'
|
|
9
|
-
import type { extensions } from '@toa.io/core/types'
|
|
10
|
-
|
|
11
|
-
export class Factory implements extensions.Factory {
|
|
12
|
-
private readonly host: Host
|
|
13
|
-
|
|
14
|
-
/** one per component, however much of this extension calls through it */
|
|
15
|
-
private readonly locals: Record<string, Local> = {}
|
|
16
|
-
|
|
17
|
-
public constructor(host: Host) {
|
|
18
|
-
this.host = host
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* What runs beside a component. Its own component gets the dispatcher; everyone else gets a
|
|
23
|
-
* timer per operation they declared a cadence for.
|
|
24
|
-
*
|
|
25
|
-
* A tenant is created before the components of its composition are, which is why nothing here
|
|
26
|
-
* reaches for one.
|
|
27
|
-
*/
|
|
28
|
-
public tenant(locator: Locator, declaration: object): Connector {
|
|
29
|
-
if (mine(locator))
|
|
30
|
-
return new Dispatcher(
|
|
31
|
-
this.metronome(),
|
|
32
|
-
(target) => this.local(target),
|
|
33
|
-
this.host.atom(locator.id)
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
const tenant = new Connector()
|
|
37
|
-
const pulses = declaration as Declaration | null
|
|
38
|
-
|
|
39
|
-
// `cadence: ~` is a component that only delays calls
|
|
40
|
-
if (pulses === null) return tenant
|
|
41
|
-
|
|
42
|
-
const local = this.local(locator)
|
|
43
|
-
const atom = this.host.atom(locator.id)
|
|
44
|
-
|
|
45
|
-
for (const [endpoint, { cycle, intervals }] of Object.entries(pulses))
|
|
46
|
-
tenant.depends(new Pulse({ locator, endpoint, cycle, intervals }, local, atom))
|
|
47
|
-
|
|
48
|
-
return tenant
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** `context.delay` — for every component but the one that keeps the calls. */
|
|
52
|
-
public aspect(locator: Locator): extensions.Aspect | extensions.Aspect[] {
|
|
53
|
-
if (mine(locator)) return []
|
|
54
|
-
|
|
55
|
-
return new Aspect(this.metronome())
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public service(): Connector {
|
|
59
|
-
return new Composition(this.host)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private metronome(): Local {
|
|
63
|
-
return this.local(new Locator(COMPONENT, NAMESPACE))
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private local(locator: Locator): Local {
|
|
67
|
-
return (this.locals[locator.id] ??= new Local(this.host, locator))
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Whether this is a component the extension ships rather than one of the application's. */
|
|
72
|
-
function mine(locator: Locator): boolean {
|
|
73
|
-
return locator.namespace === NAMESPACE
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export type Host = extensions.Host
|
package/source/Local.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { Connector } from '@toa.io/core'
|
|
2
|
-
import type { Host } from './Factory.js'
|
|
3
|
-
import type { Locator, Remote } from '@toa.io/core'
|
|
4
|
-
import type { Reply, Request } from '@toa.io/core/types'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The component a tenant belongs to, as something to call.
|
|
8
|
-
*
|
|
9
|
-
* Resolved on the first call and not before: a tenant is created ahead of the components of its
|
|
10
|
-
* composition, so at that point there is nothing to look up yet. A pulse calls at a boundary,
|
|
11
|
-
* which is long after everything is up.
|
|
12
|
-
*/
|
|
13
|
-
export class Local extends Connector {
|
|
14
|
-
private readonly host: Host
|
|
15
|
-
private readonly locator: Locator
|
|
16
|
-
private remote?: Promise<Remote>
|
|
17
|
-
|
|
18
|
-
public constructor(host: Host, locator: Locator) {
|
|
19
|
-
super()
|
|
20
|
-
|
|
21
|
-
this.host = host
|
|
22
|
-
this.locator = locator
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
public async invoke(endpoint: string, request: Request): Promise<Reply> {
|
|
26
|
-
this.remote ??= this.locate()
|
|
27
|
-
|
|
28
|
-
return await (await this.remote).invoke(endpoint, request)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private async locate(): Promise<Remote> {
|
|
32
|
-
const remote = await this.host.remote(this.locator)
|
|
33
|
-
|
|
34
|
-
this.depends(remote)
|
|
35
|
-
|
|
36
|
-
await remote.connect()
|
|
37
|
-
|
|
38
|
-
return remote
|
|
39
|
-
}
|
|
40
|
-
}
|