@toa.io/extensions.cadence 1.0.0-alpha.284
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/components/cadence.metronome/manifest.toa.yaml +72 -0
- package/components/cadence.metronome/operations/delay.js +28 -0
- package/components/cadence.metronome/operations/settle.js +9 -0
- package/components/cadence.metronome/types/index.d.ts +3 -0
- package/components/cadence.metronome/types/toa.d.ts +45 -0
- package/package.json +28 -0
- package/readme.md +188 -0
- package/schemas/annotation.cos.yaml +9 -0
- package/schemas/declaration.cos.yaml +21 -0
- package/source/Aspect.ts +64 -0
- package/source/Composition.ts +47 -0
- package/source/Dispatcher.test.ts +271 -0
- package/source/Dispatcher.ts +221 -0
- package/source/Factory.ts +73 -0
- package/source/Local.ts +39 -0
- package/source/Pulse.test.ts +274 -0
- package/source/Pulse.ts +202 -0
- package/source/const.ts +23 -0
- package/source/context.ts +10 -0
- package/source/deployment.ts +33 -0
- package/source/index.ts +8 -0
- package/source/manifest.test.ts +58 -0
- package/source/manifest.ts +51 -0
- package/source/schemas.ts +10 -0
- package/source/types.ts +41 -0
- package/transpiled/Aspect.d.ts +26 -0
- package/transpiled/Aspect.js +46 -0
- package/transpiled/Aspect.js.map +1 -0
- package/transpiled/Composition.d.ts +14 -0
- package/transpiled/Composition.js +30 -0
- package/transpiled/Composition.js.map +1 -0
- package/transpiled/Dispatcher.d.ts +63 -0
- package/transpiled/Dispatcher.js +170 -0
- package/transpiled/Dispatcher.js.map +1 -0
- package/transpiled/Factory.d.ts +22 -0
- package/transpiled/Factory.js +56 -0
- package/transpiled/Factory.js.map +1 -0
- package/transpiled/Local.d.ts +18 -0
- package/transpiled/Local.js +29 -0
- package/transpiled/Local.js.map +1 -0
- package/transpiled/Pulse.d.ts +64 -0
- package/transpiled/Pulse.js +148 -0
- package/transpiled/Pulse.js.map +1 -0
- package/transpiled/const.d.ts +15 -0
- package/transpiled/const.js +19 -0
- package/transpiled/const.js.map +1 -0
- package/transpiled/context.d.ts +3 -0
- package/transpiled/context.js +9 -0
- package/transpiled/context.js.map +1 -0
- package/transpiled/deployment.d.ts +9 -0
- package/transpiled/deployment.js +26 -0
- package/transpiled/deployment.js.map +1 -0
- package/transpiled/index.d.ts +7 -0
- package/transpiled/index.js +7 -0
- package/transpiled/index.js.map +1 -0
- package/transpiled/manifest.d.ts +11 -0
- package/transpiled/manifest.js +39 -0
- package/transpiled/manifest.js.map +1 -0
- package/transpiled/schemas.d.ts +4 -0
- package/transpiled/schemas.js +7 -0
- package/transpiled/schemas.js.map +1 -0
- package/transpiled/types.d.ts +34 -0
- package/transpiled/types.js +2 -0
- package/transpiled/types.js.map +1 -0
- package/tsconfig.json +13 -0
package/source/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** What one component declares under `cadence:`, by the operation each one calls. */
|
|
2
|
+
export type Declaration = Record<string, Pulse>
|
|
3
|
+
|
|
4
|
+
export interface Pulse {
|
|
5
|
+
/** seconds one whole cycle takes */
|
|
6
|
+
cycle: number
|
|
7
|
+
|
|
8
|
+
/** intervals the cycle is split into; the gap between calls is `cycle / intervals` */
|
|
9
|
+
intervals: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `context.delay` — a call to be made later, answering the id that cancels it. */
|
|
13
|
+
export interface Delay {
|
|
14
|
+
(endpoint: string, request: object | null, options: Options): Promise<string>
|
|
15
|
+
|
|
16
|
+
/** Raises where the id was never issued. */
|
|
17
|
+
cancel: (id: string) => Promise<void>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Options {
|
|
21
|
+
/** milliseconds from now */
|
|
22
|
+
interval: number
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Milliseconds the call may be late and still be made, or `null` for no bound at all.
|
|
26
|
+
*
|
|
27
|
+
* Stated rather than defaulted, because only the caller knows whether a late call is still
|
|
28
|
+
* the right call: an unpaid order expires on time or not at all, and a report is wanted
|
|
29
|
+
* whenever it can be had. Nothing else in the system can tell those apart.
|
|
30
|
+
*
|
|
31
|
+
* An upper bound on lateness, not a promise of timeliness. It is bounded below by how often
|
|
32
|
+
* a dispatcher scans, so a bound shorter than that is one nothing can honour.
|
|
33
|
+
*/
|
|
34
|
+
overdue: number | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What an application states under `cadence:` in its context. */
|
|
38
|
+
export interface Annotation {
|
|
39
|
+
/** seconds between passes over the calls waiting to be made */
|
|
40
|
+
discreteness?: number
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
import type { Local } from './Local.js';
|
|
3
|
+
/**
|
|
4
|
+
* `context.delay(endpoint, request, delay)` — a call to be made later, handed to the component
|
|
5
|
+
* that keeps them.
|
|
6
|
+
*
|
|
7
|
+
* It is not transactional with the caller's state. A crash between committing a change and
|
|
8
|
+
* handing over the call it should have scheduled loses the call, and there is no way for this
|
|
9
|
+
* to know. Where the semantics allow it, hand the call over first.
|
|
10
|
+
*
|
|
11
|
+
* @implements {toa.core.extensions.Aspect}
|
|
12
|
+
*/
|
|
13
|
+
export declare class Aspect extends Connector {
|
|
14
|
+
readonly name = "delay";
|
|
15
|
+
private readonly metronome;
|
|
16
|
+
constructor(metronome: Local);
|
|
17
|
+
invoke(method: string, ...args: any[]): Promise<unknown>;
|
|
18
|
+
/** Answers the id the call was given, which is what cancels it. */
|
|
19
|
+
private delay;
|
|
20
|
+
/**
|
|
21
|
+
* Tombstones the row, which is the prototype's `terminate`. An id that was never issued has
|
|
22
|
+
* nothing to terminate and raises; one whose call has already been made has a row still, and
|
|
23
|
+
* terminating it again is not an error.
|
|
24
|
+
*/
|
|
25
|
+
private cancel;
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
/**
|
|
3
|
+
* `context.delay(endpoint, request, delay)` — a call to be made later, handed to the component
|
|
4
|
+
* that keeps them.
|
|
5
|
+
*
|
|
6
|
+
* It is not transactional with the caller's state. A crash between committing a change and
|
|
7
|
+
* handing over the call it should have scheduled loses the call, and there is no way for this
|
|
8
|
+
* to know. Where the semantics allow it, hand the call over first.
|
|
9
|
+
*
|
|
10
|
+
* @implements {toa.core.extensions.Aspect}
|
|
11
|
+
*/
|
|
12
|
+
export class Aspect extends Connector {
|
|
13
|
+
name = 'delay';
|
|
14
|
+
metronome;
|
|
15
|
+
constructor(metronome) {
|
|
16
|
+
super();
|
|
17
|
+
this.metronome = metronome;
|
|
18
|
+
this.depends(metronome);
|
|
19
|
+
}
|
|
20
|
+
// named rather than forwarded, so that the connector's lifecycle is not reachable from an
|
|
21
|
+
// algorithm — the same reason the atom aspect names its own
|
|
22
|
+
async invoke(method, ...args) {
|
|
23
|
+
switch (method) {
|
|
24
|
+
case 'delay': return await this.delay(args[0], args[1], args[2]);
|
|
25
|
+
case 'cancel': return await this.cancel(args[0]);
|
|
26
|
+
default: throw new Error(`Delay aspect has no '${method}' method`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Answers the id the call was given, which is what cancels it. */
|
|
30
|
+
async delay(endpoint, request, options) {
|
|
31
|
+
const input = { endpoint, interval: options.interval, overdue: options.overdue };
|
|
32
|
+
// a call that takes no request carries none, rather than a null one
|
|
33
|
+
if (request !== null && request !== undefined)
|
|
34
|
+
input.request = request;
|
|
35
|
+
return await this.metronome.invoke('delay', { input });
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Tombstones the row, which is the prototype's `terminate`. An id that was never issued has
|
|
39
|
+
* nothing to terminate and raises; one whose call has already been made has a row still, and
|
|
40
|
+
* terminating it again is not an error.
|
|
41
|
+
*/
|
|
42
|
+
async cancel(id) {
|
|
43
|
+
return await this.metronome.invoke('terminate', { query: { id }, input: null });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=Aspect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Aspect.js","sourceRoot":"","sources":["../source/Aspect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAWxC;;;;;;;;;GASG;AACH,MAAM,OAAO,MAAO,SAAQ,SAAS;IACnB,IAAI,GAAG,OAAO,CAAA;IAEb,SAAS,CAAO;IAEjC,YAAoB,SAAgB;QAClC,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAE1B,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IACzB,CAAC;IAED,0FAA0F;IAC1F,4DAA4D;IACrD,KAAK,CAAC,MAAM,CAAE,MAAc,EAAE,GAAG,IAAW;QACjD,QAAQ,MAAM,EAAE,CAAC;YACf,KAAK,OAAO,EAAE,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YAChE,KAAK,QAAQ,EAAE,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YAChD,SAAS,MAAM,IAAI,KAAK,CAAC,wBAAwB,MAAM,UAAU,CAAC,CAAA;QACpE,CAAC;IACH,CAAC;IAED,mEAAmE;IAC3D,KAAK,CAAC,KAAK,CAAE,QAAgB,EAAE,OAAsB,EAC3D,OAAgB;QAChB,MAAM,KAAK,GAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAA;QAEvF,oEAAoE;QACpE,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS;YAAE,KAAK,CAAC,OAAO,GAAG,OAAO,CAAA;QAEtE,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;IACxD,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,MAAM,CAAE,EAAU;QAC9B,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACjF,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
import type { Host } from './Factory.js';
|
|
3
|
+
/** The components this extension ships, which run inside its own service. */
|
|
4
|
+
export declare function components(): Components;
|
|
5
|
+
export declare class Composition extends Connector {
|
|
6
|
+
private readonly host;
|
|
7
|
+
constructor(host: Host);
|
|
8
|
+
protected open(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
interface Components {
|
|
11
|
+
labels: string[];
|
|
12
|
+
paths: string[];
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { Connector } from '@toa.io/core';
|
|
4
|
+
/** The components this extension ships, which run inside its own service. */
|
|
5
|
+
export function components() {
|
|
6
|
+
const labels = [];
|
|
7
|
+
const paths = [];
|
|
8
|
+
for (const entry of entries()) {
|
|
9
|
+
labels.push(entry.name.replace('.', '-'));
|
|
10
|
+
paths.push(resolve(ROOT, entry.name));
|
|
11
|
+
}
|
|
12
|
+
return { labels, paths };
|
|
13
|
+
}
|
|
14
|
+
export class Composition extends Connector {
|
|
15
|
+
host;
|
|
16
|
+
constructor(host) {
|
|
17
|
+
super();
|
|
18
|
+
this.host = host;
|
|
19
|
+
}
|
|
20
|
+
async open() {
|
|
21
|
+
const composition = await this.host.composition(components().paths);
|
|
22
|
+
await composition.connect();
|
|
23
|
+
this.depends(composition);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function entries() {
|
|
27
|
+
return readdirSync(ROOT, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
28
|
+
}
|
|
29
|
+
const ROOT = resolve(import.meta.dirname, '../components/');
|
|
30
|
+
//# sourceMappingURL=Composition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Composition.js","sourceRoot":"","sources":["../source/Composition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAIxC,6EAA6E;AAC7E,MAAM,UAAU,UAAU;IACxB,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;QACzC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IACvC,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;AAC1B,CAAC;AAED,MAAM,OAAO,WAAY,SAAQ,SAAS;IACvB,IAAI,CAAM;IAE3B,YAAoB,IAAU;QAC5B,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAA;QAEnE,MAAM,WAAW,CAAC,OAAO,EAAE,CAAA;QAE3B,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC3B,CAAC;CACF;AAED,SAAS,OAAO;IACd,OAAO,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;AAC1F,CAAC;AAOD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Connector, Locator } from '@toa.io/core';
|
|
2
|
+
import type { Local } from './Local.js';
|
|
3
|
+
import type { atomicity } from '@toa.io/core';
|
|
4
|
+
/**
|
|
5
|
+
* Makes the calls that were put off.
|
|
6
|
+
*
|
|
7
|
+
* Two tiers, which is what a store polled rarely and a call due to the second need between
|
|
8
|
+
* them: the rows are the record, and one scan primes the next interval of them into timers.
|
|
9
|
+
* A crash loses the timers and nothing else — the next scan reads what is overdue along with
|
|
10
|
+
* what is upcoming, so a dispatcher that died mid-interval costs a delay and not a call.
|
|
11
|
+
*
|
|
12
|
+
* Partitioned by lane exactly as the outbox is, so every replica scans and dispatches its own
|
|
13
|
+
* share of every interval rather than taking turns at whole ones.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Dispatcher extends Connector {
|
|
16
|
+
private readonly metronome;
|
|
17
|
+
private readonly resolve;
|
|
18
|
+
private readonly atom;
|
|
19
|
+
/** rows in this replica's hands — a timer set for them, or a call of theirs on its way */
|
|
20
|
+
private readonly armed;
|
|
21
|
+
/** ids called and not yet removed, held until a scan settles them */
|
|
22
|
+
private called;
|
|
23
|
+
/** what it has called through, so that each is torn down with it */
|
|
24
|
+
private readonly targets;
|
|
25
|
+
/** milliseconds between passes, and how far ahead each one reaches */
|
|
26
|
+
private readonly discreteness;
|
|
27
|
+
private timer?;
|
|
28
|
+
private off?;
|
|
29
|
+
private scanning;
|
|
30
|
+
private closing;
|
|
31
|
+
constructor(metronome: Local, resolve: (locator: Locator) => Local, atom: atomicity.Atom);
|
|
32
|
+
protected open(): Promise<void>;
|
|
33
|
+
protected close(): Promise<void>;
|
|
34
|
+
/** One scan at a time: a slow one must not have another started underneath it. */
|
|
35
|
+
private tick;
|
|
36
|
+
/**
|
|
37
|
+
* What this replica owns has changed. Timers for lanes it no longer holds are dropped —
|
|
38
|
+
* whoever holds them now will arm them — and a scan runs at once rather than at the next
|
|
39
|
+
* interval, so lanes just acquired are not left waiting.
|
|
40
|
+
*/
|
|
41
|
+
private adopt;
|
|
42
|
+
/** What is armed for a lane this replica no longer holds: whoever holds it now will arm it. */
|
|
43
|
+
private disarm;
|
|
44
|
+
private scan;
|
|
45
|
+
/**
|
|
46
|
+
* A row already armed is left alone: a scan reads what it read before, because a row stays
|
|
47
|
+
* until its call has been made.
|
|
48
|
+
*/
|
|
49
|
+
private arm;
|
|
50
|
+
/**
|
|
51
|
+
* The call goes out before the row is removed, never the other way round: a crash in between
|
|
52
|
+
* makes the call twice, and the other order would lose it. It travels as a task, so the
|
|
53
|
+
* broker holds it if the target is not there to take it.
|
|
54
|
+
*/
|
|
55
|
+
private dispatch;
|
|
56
|
+
/**
|
|
57
|
+
* A remote is resolved once per target and held: it opens a connection, and one nothing
|
|
58
|
+
* depends on would outlive the dispatcher that made it.
|
|
59
|
+
*/
|
|
60
|
+
private target;
|
|
61
|
+
/** One write for many rows, which is why the ids are held rather than settled one by one. */
|
|
62
|
+
private settle;
|
|
63
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { console } from 'openspan';
|
|
2
|
+
import { Connector, Locator } from '@toa.io/core';
|
|
3
|
+
import { BATCH, DISCRETENESS, LANES, number } from './const.js';
|
|
4
|
+
/**
|
|
5
|
+
* Makes the calls that were put off.
|
|
6
|
+
*
|
|
7
|
+
* Two tiers, which is what a store polled rarely and a call due to the second need between
|
|
8
|
+
* them: the rows are the record, and one scan primes the next interval of them into timers.
|
|
9
|
+
* A crash loses the timers and nothing else — the next scan reads what is overdue along with
|
|
10
|
+
* what is upcoming, so a dispatcher that died mid-interval costs a delay and not a call.
|
|
11
|
+
*
|
|
12
|
+
* Partitioned by lane exactly as the outbox is, so every replica scans and dispatches its own
|
|
13
|
+
* share of every interval rather than taking turns at whole ones.
|
|
14
|
+
*/
|
|
15
|
+
export class Dispatcher extends Connector {
|
|
16
|
+
metronome;
|
|
17
|
+
resolve;
|
|
18
|
+
atom;
|
|
19
|
+
/** rows in this replica's hands — a timer set for them, or a call of theirs on its way */
|
|
20
|
+
armed = new Map();
|
|
21
|
+
/** ids called and not yet removed, held until a scan settles them */
|
|
22
|
+
called = new Set();
|
|
23
|
+
/** what it has called through, so that each is torn down with it */
|
|
24
|
+
targets = new Set();
|
|
25
|
+
/** milliseconds between passes, and how far ahead each one reaches */
|
|
26
|
+
discreteness;
|
|
27
|
+
timer;
|
|
28
|
+
off;
|
|
29
|
+
scanning = false;
|
|
30
|
+
closing = false;
|
|
31
|
+
constructor(metronome, resolve, atom) {
|
|
32
|
+
super();
|
|
33
|
+
this.metronome = metronome;
|
|
34
|
+
this.resolve = resolve;
|
|
35
|
+
this.atom = atom;
|
|
36
|
+
this.discreteness = number('TOA_CADENCE_DISCRETENESS', DISCRETENESS * 1000);
|
|
37
|
+
this.depends(metronome);
|
|
38
|
+
this.depends(atom);
|
|
39
|
+
}
|
|
40
|
+
async open() {
|
|
41
|
+
this.timer = setInterval(() => { this.tick(); }, this.discreteness);
|
|
42
|
+
this.timer.unref();
|
|
43
|
+
// a lane changing hands is when its rows become this replica's, or stop being
|
|
44
|
+
this.off = this.atom.onassigned(() => { this.adopt(); });
|
|
45
|
+
}
|
|
46
|
+
async close() {
|
|
47
|
+
this.closing = true;
|
|
48
|
+
this.off?.();
|
|
49
|
+
clearInterval(this.timer);
|
|
50
|
+
for (const { timer } of this.armed.values())
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
this.armed.clear();
|
|
53
|
+
await this.settle();
|
|
54
|
+
}
|
|
55
|
+
/** One scan at a time: a slow one must not have another started underneath it. */
|
|
56
|
+
tick() {
|
|
57
|
+
if (this.scanning || this.closing)
|
|
58
|
+
return;
|
|
59
|
+
this.scanning = true;
|
|
60
|
+
void this.scan()
|
|
61
|
+
.catch((error) => { console.error('Delayed calls scan failed', { error }); })
|
|
62
|
+
.finally(() => { this.scanning = false; });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* What this replica owns has changed. Timers for lanes it no longer holds are dropped —
|
|
66
|
+
* whoever holds them now will arm them — and a scan runs at once rather than at the next
|
|
67
|
+
* interval, so lanes just acquired are not left waiting.
|
|
68
|
+
*/
|
|
69
|
+
adopt() {
|
|
70
|
+
// nothing owned is nothing held: what was armed under a claim this replica no longer has
|
|
71
|
+
// belongs to whoever has it now, and firing it anyway is the call twice
|
|
72
|
+
this.disarm(this.atom.slots(LANES) ?? []);
|
|
73
|
+
this.tick();
|
|
74
|
+
}
|
|
75
|
+
/** What is armed for a lane this replica no longer holds: whoever holds it now will arm it. */
|
|
76
|
+
disarm(lanes) {
|
|
77
|
+
for (const [id, { timer, lane }] of this.armed) {
|
|
78
|
+
if (lanes.includes(lane))
|
|
79
|
+
continue;
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
this.armed.delete(id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async scan() {
|
|
85
|
+
const lanes = this.atom.slots(LANES);
|
|
86
|
+
if (lanes === null || lanes.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
await this.settle();
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const until = now + this.discreteness;
|
|
91
|
+
const rows = await this.metronome.invoke('enumerate', {
|
|
92
|
+
query: {
|
|
93
|
+
// a call nothing was running to make in time has expired, and is simply not read
|
|
94
|
+
criteria: `lane=in=(${lanes.join(',')});due<${until};expires>${now}`,
|
|
95
|
+
sort: ['due:asc'],
|
|
96
|
+
limit: BATCH
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
for (const row of rows)
|
|
100
|
+
this.arm(row);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A row already armed is left alone: a scan reads what it read before, because a row stays
|
|
104
|
+
* until its call has been made.
|
|
105
|
+
*/
|
|
106
|
+
arm(row) {
|
|
107
|
+
if (this.armed.has(row.id) || this.called.has(row.id))
|
|
108
|
+
return;
|
|
109
|
+
const timer = setTimeout(() => { void this.dispatch(row); }, Math.max(row.due - Date.now(), 0));
|
|
110
|
+
timer.unref();
|
|
111
|
+
this.armed.set(row.id, { timer, lane: row.lane });
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The call goes out before the row is removed, never the other way round: a crash in between
|
|
115
|
+
* makes the call twice, and the other order would lose it. It travels as a task, so the
|
|
116
|
+
* broker holds it if the target is not there to take it.
|
|
117
|
+
*/
|
|
118
|
+
async dispatch(row) {
|
|
119
|
+
if (this.closing) {
|
|
120
|
+
this.armed.delete(row.id);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const [namespace, name, endpoint] = row.endpoint.split('.');
|
|
124
|
+
const local = this.target(new Locator(name, namespace));
|
|
125
|
+
/*
|
|
126
|
+
* Attempted before it is made, not after it comes back. One attempt is what a dispatcher
|
|
127
|
+
* has to give: what it can tell about a failure is nothing, and a request the target's
|
|
128
|
+
* schema no longer fits never becomes one it does.
|
|
129
|
+
*/
|
|
130
|
+
this.called.add(row.id);
|
|
131
|
+
/*
|
|
132
|
+
* The call travels as a task, so what raises here is this side of it: a stored request the
|
|
133
|
+
* target's contract no longer fits, an endpoint it no longer has, a broker that refused the
|
|
134
|
+
* enqueue. What the operation itself does with it happens in the target's own process and
|
|
135
|
+
* never comes back.
|
|
136
|
+
*
|
|
137
|
+
* Skipped, as a pulse skips: reported, and the row settled on the attempt either way.
|
|
138
|
+
*/
|
|
139
|
+
await local.invoke(endpoint, { input: null, ...row.request, task: true })
|
|
140
|
+
.catch((error) => {
|
|
141
|
+
console.error('Delayed call failed', { endpoint: row.endpoint, id: row.id, error });
|
|
142
|
+
})
|
|
143
|
+
// held for as long as the call is, so `armed` answers what this replica has in hand
|
|
144
|
+
// rather than what it has a timer for
|
|
145
|
+
.finally(() => { this.armed.delete(row.id); });
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A remote is resolved once per target and held: it opens a connection, and one nothing
|
|
149
|
+
* depends on would outlive the dispatcher that made it.
|
|
150
|
+
*/
|
|
151
|
+
target(locator) {
|
|
152
|
+
const local = this.resolve(locator);
|
|
153
|
+
if (!this.targets.has(local)) {
|
|
154
|
+
this.targets.add(local);
|
|
155
|
+
this.depends(local);
|
|
156
|
+
}
|
|
157
|
+
return local;
|
|
158
|
+
}
|
|
159
|
+
/** One write for many rows, which is why the ids are held rather than settled one by one. */
|
|
160
|
+
async settle() {
|
|
161
|
+
if (this.called.size === 0)
|
|
162
|
+
return;
|
|
163
|
+
const ids = [...this.called];
|
|
164
|
+
await this.metronome.invoke('settle', { query: { ids }, input: null });
|
|
165
|
+
// held until the write comes back, so that one which does not leaves them to a later scan
|
|
166
|
+
for (const id of ids)
|
|
167
|
+
this.called.delete(id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=Dispatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Dispatcher.js","sourceRoot":"","sources":["../source/Dispatcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Connector, Locator } from '@toa.io/core';
|
|
2
|
+
import type { extensions } from '@toa.io/core';
|
|
3
|
+
export declare class Factory implements extensions.Factory {
|
|
4
|
+
private readonly host;
|
|
5
|
+
/** one per component, however much of this extension calls through it */
|
|
6
|
+
private readonly locals;
|
|
7
|
+
constructor(host: Host);
|
|
8
|
+
/**
|
|
9
|
+
* What runs beside a component. Its own component gets the dispatcher; everyone else gets a
|
|
10
|
+
* timer per operation they declared a cadence for.
|
|
11
|
+
*
|
|
12
|
+
* A tenant is created before the components of its composition are, which is why nothing here
|
|
13
|
+
* reaches for one.
|
|
14
|
+
*/
|
|
15
|
+
tenant(locator: Locator, declaration: object): Connector;
|
|
16
|
+
/** `context.delay` — for every component but the one that keeps the calls. */
|
|
17
|
+
aspect(locator: Locator): extensions.Aspect | extensions.Aspect[];
|
|
18
|
+
service(): Connector;
|
|
19
|
+
private metronome;
|
|
20
|
+
private local;
|
|
21
|
+
}
|
|
22
|
+
export type Host = extensions.Host;
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
export class Factory {
|
|
9
|
+
host;
|
|
10
|
+
/** one per component, however much of this extension calls through it */
|
|
11
|
+
locals = {};
|
|
12
|
+
constructor(host) {
|
|
13
|
+
this.host = host;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What runs beside a component. Its own component gets the dispatcher; everyone else gets a
|
|
17
|
+
* timer per operation they declared a cadence for.
|
|
18
|
+
*
|
|
19
|
+
* A tenant is created before the components of its composition are, which is why nothing here
|
|
20
|
+
* reaches for one.
|
|
21
|
+
*/
|
|
22
|
+
tenant(locator, declaration) {
|
|
23
|
+
if (mine(locator))
|
|
24
|
+
return new Dispatcher(this.metronome(), (target) => this.local(target), this.host.atom(locator.id));
|
|
25
|
+
const tenant = new Connector();
|
|
26
|
+
const pulses = declaration;
|
|
27
|
+
// `cadence: ~` is a component that only delays calls
|
|
28
|
+
if (pulses === null)
|
|
29
|
+
return tenant;
|
|
30
|
+
const local = this.local(locator);
|
|
31
|
+
const atom = this.host.atom(locator.id);
|
|
32
|
+
for (const [endpoint, { cycle, intervals }] of Object.entries(pulses))
|
|
33
|
+
tenant.depends(new Pulse({ locator, endpoint, cycle, intervals }, local, atom));
|
|
34
|
+
return tenant;
|
|
35
|
+
}
|
|
36
|
+
/** `context.delay` — for every component but the one that keeps the calls. */
|
|
37
|
+
aspect(locator) {
|
|
38
|
+
if (mine(locator))
|
|
39
|
+
return [];
|
|
40
|
+
return new Aspect(this.metronome());
|
|
41
|
+
}
|
|
42
|
+
service() {
|
|
43
|
+
return new Composition(this.host);
|
|
44
|
+
}
|
|
45
|
+
metronome() {
|
|
46
|
+
return this.local(new Locator(COMPONENT, NAMESPACE));
|
|
47
|
+
}
|
|
48
|
+
local(locator) {
|
|
49
|
+
return (this.locals[locator.id] ??= new Local(this.host, locator));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Whether this is a component the extension ships rather than one of the application's. */
|
|
53
|
+
function mine(locator) {
|
|
54
|
+
return locator.namespace === NAMESPACE;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=Factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAIlC,MAAM,OAAO,OAAO;IACD,IAAI,CAAM;IAE3B,yEAAyE;IACxD,MAAM,GAA0B,EAAE,CAAA;IAEnD,YAAoB,IAAU;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAED;;;;;;OAMG;IACI,MAAM,CAAE,OAAgB,EAAE,WAAmB;QAClD,IAAI,IAAI,CAAC,OAAO,CAAC;YACf,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EACpE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;QAE/B,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAA;QAC9B,MAAM,MAAM,GAAG,WAAiC,CAAA;QAEhD,qDAAqD;QACrD,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,MAAM,CAAA;QAElC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACjC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAEvC,KAAK,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YACnE,MAAM,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAA;QAEjF,OAAO,MAAM,CAAA;IACf,CAAC;IAED,8EAA8E;IACvE,MAAM,CAAE,OAAgB;QAC7B,IAAI,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAA;QAE5B,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAA;IACrC,CAAC;IAEM,OAAO;QACZ,OAAO,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACnC,CAAC;IAEO,SAAS;QACf,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAA;IACtD,CAAC;IAEO,KAAK,CAAE,OAAgB;QAC7B,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAA;IACpE,CAAC;CACF;AAED,4FAA4F;AAC5F,SAAS,IAAI,CAAE,OAAgB;IAC7B,OAAO,OAAO,CAAC,SAAS,KAAK,SAAS,CAAA;AACxC,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
import type { Host } from './Factory.js';
|
|
3
|
+
import type { Locator, Reply, Request } from '@toa.io/core';
|
|
4
|
+
/**
|
|
5
|
+
* The component a tenant belongs to, as something to call.
|
|
6
|
+
*
|
|
7
|
+
* Resolved on the first call and not before: a tenant is created ahead of the components of its
|
|
8
|
+
* composition, so at that point there is nothing to look up yet. A pulse calls at a boundary,
|
|
9
|
+
* which is long after everything is up.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Local extends Connector {
|
|
12
|
+
private readonly host;
|
|
13
|
+
private readonly locator;
|
|
14
|
+
private remote?;
|
|
15
|
+
constructor(host: Host, locator: Locator);
|
|
16
|
+
invoke(endpoint: string, request: Request): Promise<Reply>;
|
|
17
|
+
private locate;
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
/**
|
|
3
|
+
* The component a tenant belongs to, as something to call.
|
|
4
|
+
*
|
|
5
|
+
* Resolved on the first call and not before: a tenant is created ahead of the components of its
|
|
6
|
+
* composition, so at that point there is nothing to look up yet. A pulse calls at a boundary,
|
|
7
|
+
* which is long after everything is up.
|
|
8
|
+
*/
|
|
9
|
+
export class Local extends Connector {
|
|
10
|
+
host;
|
|
11
|
+
locator;
|
|
12
|
+
remote;
|
|
13
|
+
constructor(host, locator) {
|
|
14
|
+
super();
|
|
15
|
+
this.host = host;
|
|
16
|
+
this.locator = locator;
|
|
17
|
+
}
|
|
18
|
+
async invoke(endpoint, request) {
|
|
19
|
+
this.remote ??= this.locate();
|
|
20
|
+
return await (await this.remote).invoke(endpoint, request);
|
|
21
|
+
}
|
|
22
|
+
async locate() {
|
|
23
|
+
const remote = await this.host.remote(this.locator);
|
|
24
|
+
this.depends(remote);
|
|
25
|
+
await remote.connect();
|
|
26
|
+
return remote;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=Local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Local.js","sourceRoot":"","sources":["../source/Local.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAIxC;;;;;;GAMG;AACH,MAAM,OAAO,KAAM,SAAQ,SAAS;IACjB,IAAI,CAAM;IACV,OAAO,CAAS;IACzB,MAAM,CAAkB;IAEhC,YAAoB,IAAU,EAAE,OAAgB;QAC9C,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;IAEM,KAAK,CAAC,MAAM,CAAE,QAAgB,EAAE,OAAgB;QACrD,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,CAAA;QAE7B,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC5D,CAAC;IAEO,KAAK,CAAC,MAAM;QAClB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAEnD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAEpB,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA;QAEtB,OAAO,MAAM,CAAA;IACf,CAAC;CACF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
import type { Local } from './Local.js';
|
|
3
|
+
import type { Locator, atomicity } from '@toa.io/core';
|
|
4
|
+
/**
|
|
5
|
+
* A recurring call to an operation of its own component, on the cadence its manifest states.
|
|
6
|
+
*
|
|
7
|
+
* Nothing is stored. The cycle is split into `n` intervals and the current one is a function of
|
|
8
|
+
* the clock, so every replica computes the same `i` without talking to any other; the one that
|
|
9
|
+
* owns `i` is the one that calls. An interval no replica was alive for is not made up
|
|
10
|
+
* afterwards — the operation is expected to select by what is still due rather than by where
|
|
11
|
+
* the cycle has reached.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Pulse extends Connector {
|
|
14
|
+
private readonly endpoint;
|
|
15
|
+
private readonly label;
|
|
16
|
+
/** milliseconds one whole cycle takes */
|
|
17
|
+
private readonly cycle;
|
|
18
|
+
/** intervals the cycle is split into */
|
|
19
|
+
private readonly n;
|
|
20
|
+
private readonly local;
|
|
21
|
+
private readonly atom;
|
|
22
|
+
/** span options, built once */
|
|
23
|
+
private readonly options;
|
|
24
|
+
/**
|
|
25
|
+
* The ordinal of the interval this process has already handled, so an early wake-up is not a
|
|
26
|
+
* second call. An ordinal and not an index: with one interval per cycle the index never
|
|
27
|
+
* changes, and comparing it would suppress every call after the first.
|
|
28
|
+
*/
|
|
29
|
+
private fired;
|
|
30
|
+
private timer?;
|
|
31
|
+
/** the call in flight, awaited on close */
|
|
32
|
+
private firing?;
|
|
33
|
+
private closing;
|
|
34
|
+
constructor(definition: Definition, local: Local, atom: atomicity.Atom);
|
|
35
|
+
/**
|
|
36
|
+
* How many intervals have passed since the epoch, `now` included. Against the cycle rather
|
|
37
|
+
* than by adding up gaps, so a cycle that does not divide evenly into its intervals neither
|
|
38
|
+
* drifts nor accumulates error.
|
|
39
|
+
*
|
|
40
|
+
* The arithmetic is exact rather than double: `now` is milliseconds since 1970 and `n` may be
|
|
41
|
+
* as large as the cycle has seconds, so their product leaves the range a double is exact in
|
|
42
|
+
* for a long cycle cut finely. It is evaluated once per interval, where this costs nothing.
|
|
43
|
+
*/
|
|
44
|
+
ordinal(now: number): number;
|
|
45
|
+
/** Which of the cycle's intervals `now` falls in. */
|
|
46
|
+
index(now: number): number;
|
|
47
|
+
/** When the interval `now` falls in gives way to the next one. */
|
|
48
|
+
boundary(now: number): number;
|
|
49
|
+
protected open(): Promise<void>;
|
|
50
|
+
protected close(): Promise<void>;
|
|
51
|
+
private arm;
|
|
52
|
+
private tick;
|
|
53
|
+
/**
|
|
54
|
+
* Nothing queues here. A call still running when the next boundary arrives means the work
|
|
55
|
+
* does not fit the gap, and starting a second one would only make that worse.
|
|
56
|
+
*/
|
|
57
|
+
private fire;
|
|
58
|
+
}
|
|
59
|
+
export interface Definition {
|
|
60
|
+
locator: Locator;
|
|
61
|
+
endpoint: string;
|
|
62
|
+
cycle: number;
|
|
63
|
+
intervals: number;
|
|
64
|
+
}
|