@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/components/cadence.metronome/manifest.toa.yaml +72 -0
  3. package/components/cadence.metronome/operations/delay.js +28 -0
  4. package/components/cadence.metronome/operations/settle.js +9 -0
  5. package/components/cadence.metronome/types/index.d.ts +3 -0
  6. package/components/cadence.metronome/types/toa.d.ts +45 -0
  7. package/package.json +28 -0
  8. package/readme.md +188 -0
  9. package/schemas/annotation.cos.yaml +9 -0
  10. package/schemas/declaration.cos.yaml +21 -0
  11. package/source/Aspect.ts +64 -0
  12. package/source/Composition.ts +47 -0
  13. package/source/Dispatcher.test.ts +271 -0
  14. package/source/Dispatcher.ts +221 -0
  15. package/source/Factory.ts +73 -0
  16. package/source/Local.ts +39 -0
  17. package/source/Pulse.test.ts +274 -0
  18. package/source/Pulse.ts +202 -0
  19. package/source/const.ts +23 -0
  20. package/source/context.ts +10 -0
  21. package/source/deployment.ts +33 -0
  22. package/source/index.ts +8 -0
  23. package/source/manifest.test.ts +58 -0
  24. package/source/manifest.ts +51 -0
  25. package/source/schemas.ts +10 -0
  26. package/source/types.ts +41 -0
  27. package/transpiled/Aspect.d.ts +26 -0
  28. package/transpiled/Aspect.js +46 -0
  29. package/transpiled/Aspect.js.map +1 -0
  30. package/transpiled/Composition.d.ts +14 -0
  31. package/transpiled/Composition.js +30 -0
  32. package/transpiled/Composition.js.map +1 -0
  33. package/transpiled/Dispatcher.d.ts +63 -0
  34. package/transpiled/Dispatcher.js +170 -0
  35. package/transpiled/Dispatcher.js.map +1 -0
  36. package/transpiled/Factory.d.ts +22 -0
  37. package/transpiled/Factory.js +56 -0
  38. package/transpiled/Factory.js.map +1 -0
  39. package/transpiled/Local.d.ts +18 -0
  40. package/transpiled/Local.js +29 -0
  41. package/transpiled/Local.js.map +1 -0
  42. package/transpiled/Pulse.d.ts +64 -0
  43. package/transpiled/Pulse.js +148 -0
  44. package/transpiled/Pulse.js.map +1 -0
  45. package/transpiled/const.d.ts +15 -0
  46. package/transpiled/const.js +19 -0
  47. package/transpiled/const.js.map +1 -0
  48. package/transpiled/context.d.ts +3 -0
  49. package/transpiled/context.js +9 -0
  50. package/transpiled/context.js.map +1 -0
  51. package/transpiled/deployment.d.ts +9 -0
  52. package/transpiled/deployment.js +26 -0
  53. package/transpiled/deployment.js.map +1 -0
  54. package/transpiled/index.d.ts +7 -0
  55. package/transpiled/index.js +7 -0
  56. package/transpiled/index.js.map +1 -0
  57. package/transpiled/manifest.d.ts +11 -0
  58. package/transpiled/manifest.js +39 -0
  59. package/transpiled/manifest.js.map +1 -0
  60. package/transpiled/schemas.d.ts +4 -0
  61. package/transpiled/schemas.js +7 -0
  62. package/transpiled/schemas.js.map +1 -0
  63. package/transpiled/types.d.ts +34 -0
  64. package/transpiled/types.js +2 -0
  65. package/transpiled/types.js.map +1 -0
  66. package/tsconfig.json +13 -0
@@ -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
+ }