@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,148 @@
1
+ import { console } from 'openspan';
2
+ import { Connector } from '@toa.io/core';
3
+ /**
4
+ * A recurring call to an operation of its own component, on the cadence its manifest states.
5
+ *
6
+ * Nothing is stored. The cycle is split into `n` intervals and the current one is a function of
7
+ * the clock, so every replica computes the same `i` without talking to any other; the one that
8
+ * owns `i` is the one that calls. An interval no replica was alive for is not made up
9
+ * afterwards — the operation is expected to select by what is still due rather than by where
10
+ * the cycle has reached.
11
+ */
12
+ export class Pulse extends Connector {
13
+ endpoint;
14
+ label;
15
+ /** milliseconds one whole cycle takes */
16
+ cycle;
17
+ /** intervals the cycle is split into */
18
+ n;
19
+ local;
20
+ atom;
21
+ /** span options, built once */
22
+ options;
23
+ /**
24
+ * The ordinal of the interval this process has already handled, so an early wake-up is not a
25
+ * second call. An ordinal and not an index: with one interval per cycle the index never
26
+ * changes, and comparing it would suppress every call after the first.
27
+ */
28
+ fired = 0;
29
+ timer;
30
+ /** the call in flight, awaited on close */
31
+ firing;
32
+ closing = false;
33
+ constructor(definition, local, atom) {
34
+ super();
35
+ const { locator, endpoint, cycle, intervals } = definition;
36
+ this.endpoint = endpoint;
37
+ this.label = `${locator.id}.${endpoint}`;
38
+ this.cycle = cycle * 1000;
39
+ this.n = intervals;
40
+ this.local = local;
41
+ this.atom = atom;
42
+ this.depends(local);
43
+ this.depends(atom);
44
+ this.options = {
45
+ name: `${this.label} pulse`,
46
+ kind: 'producer',
47
+ service: locator.id
48
+ };
49
+ }
50
+ /**
51
+ * How many intervals have passed since the epoch, `now` included. Against the cycle rather
52
+ * than by adding up gaps, so a cycle that does not divide evenly into its intervals neither
53
+ * drifts nor accumulates error.
54
+ *
55
+ * The arithmetic is exact rather than double: `now` is milliseconds since 1970 and `n` may be
56
+ * as large as the cycle has seconds, so their product leaves the range a double is exact in
57
+ * for a long cycle cut finely. It is evaluated once per interval, where this costs nothing.
58
+ */
59
+ ordinal(now) {
60
+ return Number(BigInt(now) * BigInt(this.n) / BigInt(this.cycle));
61
+ }
62
+ /** Which of the cycle's intervals `now` falls in. */
63
+ index(now) {
64
+ return this.ordinal(now) % this.n;
65
+ }
66
+ /** When the interval `now` falls in gives way to the next one. */
67
+ boundary(now) {
68
+ const cycle = BigInt(this.cycle);
69
+ const n = BigInt(this.n);
70
+ const next = BigInt(this.ordinal(now) + 1);
71
+ // the earliest millisecond whose ordinal is the next one
72
+ return Number((next * cycle + n - 1n) / n);
73
+ }
74
+ async open() {
75
+ // whatever interval it is now, this replica was not there when it began, so it is not
76
+ // this one's to call — the first call is at the next boundary
77
+ this.fired = this.ordinal(Date.now());
78
+ this.arm();
79
+ }
80
+ async close() {
81
+ this.closing = true;
82
+ clearTimeout(this.timer);
83
+ // a call still running holds the remote it was made through, which is torn down after
84
+ // this returns. Bounded, because an operation that never returns would outlive any
85
+ // grace period and the interval is over either way
86
+ if (this.firing !== undefined)
87
+ await Promise.race([this.firing, delay(DRAIN)]);
88
+ }
89
+ arm() {
90
+ const now = Date.now();
91
+ // a delay past this fires at once rather than late, so a long cycle is waited out in
92
+ // instalments; the wake-up finds the same interval and simply arms again
93
+ const wait = Math.min(this.boundary(now) - now, MAX_DELAY);
94
+ this.timer = setTimeout(() => this.tick(), wait);
95
+ this.timer.unref();
96
+ }
97
+ tick() {
98
+ if (this.closing)
99
+ return;
100
+ const ordinal = this.ordinal(Date.now());
101
+ // a timer may wake a millisecond before the boundary it was set for, and a cycle longer
102
+ // than one timer wakes several times on the way — both are the interval already handled
103
+ if (ordinal !== this.fired) {
104
+ this.fired = ordinal;
105
+ void this.fire(ordinal % this.n);
106
+ }
107
+ this.arm();
108
+ }
109
+ /**
110
+ * Nothing queues here. A call still running when the next boundary arrives means the work
111
+ * does not fit the gap, and starting a second one would only make that worse.
112
+ */
113
+ async fire(i) {
114
+ if (this.firing !== undefined) {
115
+ console.warn('Pulse skipped: the previous call has not returned', { pulse: this.label, interval: i });
116
+ return;
117
+ }
118
+ const owned = this.atom.slots(this.n);
119
+ if (owned === null) {
120
+ console.warn('Pulse skipped: this replica owns nothing', { pulse: this.label, interval: i });
121
+ return;
122
+ }
123
+ if (!owned.includes(i))
124
+ return;
125
+ /*
126
+ * A pulse calls and waits, so an operation that raised comes back as an exception here —
127
+ * the runtime carries it across and rethrows it in this process. What to do about one is a
128
+ * choice with no right answer, and asking would put the whole of it in front of anyone who
129
+ * only wanted an operation called every hour. So the interval is skipped: it is reported,
130
+ * and the next one is called as though it had not happened.
131
+ */
132
+ const firing = console.span(this.options, async () => await this.local.invoke(this.endpoint, { input: { n: this.n, i } }))
133
+ .catch((error) => {
134
+ console.error('Pulse failed', { pulse: this.label, interval: i, error });
135
+ })
136
+ .finally(() => { this.firing = undefined; });
137
+ this.firing = firing;
138
+ await firing;
139
+ }
140
+ }
141
+ async function delay(ms) {
142
+ await new Promise((resolve) => setTimeout(resolve, ms).unref());
143
+ }
144
+ /** `setTimeout` fires immediately past this, so a longer wait is taken in instalments */
145
+ const MAX_DELAY = 2 ** 31 - 1;
146
+ /** how long a call in flight is waited for on close */
147
+ const DRAIN = 10_000;
148
+ //# sourceMappingURL=Pulse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Pulse.js","sourceRoot":"","sources":["../source/Pulse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /** The component the extension ships to hold delayed calls. */
2
+ export declare const NAMESPACE = "cadence";
3
+ export declare const COMPONENT = "metronome";
4
+ /**
5
+ * Constant, never configuration: a row carries its lane, so lowering this would leave rows in
6
+ * lanes nobody reads any more. It is also the ceiling on replicas dispatching, and a power of
7
+ * two so that the common replica counts divide evenly. The outbox says the same of its own.
8
+ */
9
+ export declare const LANES = 128;
10
+ /** seconds between passes over the calls waiting to be made, and how far ahead each reaches */
11
+ export declare const DISCRETENESS = 60;
12
+ /** rows one scan brings back */
13
+ export declare const BATCH = 200;
14
+ /** A number from the environment, where a deployment or a test suite states one. */
15
+ export declare function number(variable: string, fallback: number): number;
@@ -0,0 +1,19 @@
1
+ /** The component the extension ships to hold delayed calls. */
2
+ export const NAMESPACE = 'cadence';
3
+ export const COMPONENT = 'metronome';
4
+ /**
5
+ * Constant, never configuration: a row carries its lane, so lowering this would leave rows in
6
+ * lanes nobody reads any more. It is also the ceiling on replicas dispatching, and a power of
7
+ * two so that the common replica counts divide evenly. The outbox says the same of its own.
8
+ */
9
+ export const LANES = 128;
10
+ /** seconds between passes over the calls waiting to be made, and how far ahead each reaches */
11
+ export const DISCRETENESS = 60;
12
+ /** rows one scan brings back */
13
+ export const BATCH = 200;
14
+ /** A number from the environment, where a deployment or a test suite states one. */
15
+ export function number(variable, fallback) {
16
+ const value = Number(process.env[variable]);
17
+ return Number.isNaN(value) || value <= 0 ? fallback : value;
18
+ }
19
+ //# sourceMappingURL=const.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"const.js","sourceRoot":"","sources":["../source/const.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,CAAC,MAAM,SAAS,GAAG,SAAS,CAAA;AAClC,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAA;AAEpC;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,CAAA;AAExB,+FAA+F;AAC/F,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,CAAA;AAE9B,gCAAgC;AAChC,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,CAAA;AAExB,oFAAoF;AACpF,MAAM,UAAU,MAAM,CAAE,QAAgB,EAAE,QAAgB;IACxD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;IAE3C,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAA;AAC7D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Contribution } from '@toa.io/core';
2
+ /** What this extension puts on the context of a component that declares it. */
3
+ export declare function context(): Contribution;
@@ -0,0 +1,9 @@
1
+ /** What this extension puts on the context of a component that declares it. */
2
+ export function context() {
3
+ return {
4
+ name: 'delay',
5
+ type: 'Delay',
6
+ imports: { '@toa.io/extensions.cadence': ['Delay'] }
7
+ };
8
+ }
9
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../source/context.ts"],"names":[],"mappings":"AAEA,+EAA+E;AAC/E,MAAM,UAAU,OAAO;IACrB,OAAO;QACL,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,OAAO;QACb,OAAO,EAAE,EAAE,4BAA4B,EAAE,CAAC,OAAO,CAAC,EAAE;KACrD,CAAA;AACH,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { Annotation } from './types.js';
2
+ import type { Dependency } from '@toa.io/operations';
3
+ /**
4
+ * The component that keeps delayed calls runs in a service of its own, the way the identity
5
+ * components run in the gateway. A composition may claim it and run it in its own pods instead.
6
+ */
7
+ export declare function deployment(_: unknown, annotation?: Annotation | null): Dependency;
8
+ /** The extension is deployed for the sake of `delay`, which nothing has to declare to use. */
9
+ export declare const standalone = true;
@@ -0,0 +1,26 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { components } from './Composition.js';
3
+ import { DISCRETENESS, NAMESPACE } from './const.js';
4
+ import * as schemas from './schemas.js';
5
+ /**
6
+ * The component that keeps delayed calls runs in a service of its own, the way the identity
7
+ * components run in the gateway. A composition may claim it and run it in its own pods instead.
8
+ */
9
+ export function deployment(_, annotation) {
10
+ schemas.annotation.validate(annotation ?? null, 'Invalid cadence annotation');
11
+ // stated in seconds and carried in milliseconds, so that what an application writes is what
12
+ // the rest of a manifest is written in
13
+ const discreteness = (annotation?.discreteness ?? DISCRETENESS) * 1000;
14
+ const variables = [{ name: 'TOA_CADENCE_DISCRETENESS', value: String(discreteness) }];
15
+ const service = {
16
+ group: NAMESPACE,
17
+ name: 'metronome',
18
+ version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version,
19
+ components: components().labels,
20
+ variables
21
+ };
22
+ return { services: [service] };
23
+ }
24
+ /** The extension is deployed for the sake of `delay`, which nothing has to declare to use. */
25
+ export const standalone = true;
26
+ //# sourceMappingURL=deployment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deployment.js","sourceRoot":"","sources":["../source/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAIvC;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAE,CAAU,EAAE,UAA8B;IACpE,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAoB,UAAU,IAAI,IAAI,EAAE,4BAA4B,CAAC,CAAA;IAEhG,4FAA4F;IAC5F,uCAAuC;IACvC,MAAM,YAAY,GAAG,CAAC,UAAU,EAAE,YAAY,IAAI,YAAY,CAAC,GAAG,IAAI,CAAA;IACtE,MAAM,SAAS,GACb,CAAC,EAAE,IAAI,EAAE,0BAA0B,EAAE,KAAK,EAAE,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;IAErE,MAAM,OAAO,GAAY;QACvB,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO;QAC9F,UAAU,EAAE,UAAU,EAAE,CAAC,MAAM;QAC/B,SAAS;KACV,CAAA;IAED,OAAO,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAA;AAChC,CAAC;AAED,8FAA8F;AAC9F,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAA"}
@@ -0,0 +1,7 @@
1
+ export { Factory } from './Factory.js';
2
+ export { manifest } from './manifest.js';
3
+ export { components } from './Composition.js';
4
+ export { deployment, standalone } from './deployment.js';
5
+ export { context } from './context.js';
6
+ export { LANES } from './const.js';
7
+ export type { Declaration, Delay, Pulse } from './types.js';
@@ -0,0 +1,7 @@
1
+ export { Factory } from './Factory.js';
2
+ export { manifest } from './manifest.js';
3
+ export { components } from './Composition.js';
4
+ export { deployment, standalone } from './deployment.js';
5
+ export { context } from './context.js';
6
+ export { LANES } from './const.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { Declaration } from './types.js';
2
+ import type { Manifest } from '@toa.io/norm';
3
+ /**
4
+ * The `cadence:` block of a component manifest: the operations called on a cadence, by the
5
+ * operation each one calls. Norm hands over the whole manifest, so what the declaration refers
6
+ * to is checked here rather than left to fail at boot.
7
+ *
8
+ * A component that declares nothing still has a declaration. `cadence: ~` is how one that only
9
+ * delays calls says so, and it has to produce a value or norm rejects the extension.
10
+ */
11
+ export declare function manifest(declaration: Declaration | null | undefined, component: Manifest): Declaration;
@@ -0,0 +1,39 @@
1
+ import * as schemas from './schemas.js';
2
+ /**
3
+ * The `cadence:` block of a component manifest: the operations called on a cadence, by the
4
+ * operation each one calls. Norm hands over the whole manifest, so what the declaration refers
5
+ * to is checked here rather than left to fail at boot.
6
+ *
7
+ * A component that declares nothing still has a declaration. `cadence: ~` is how one that only
8
+ * delays calls says so, and it has to produce a value or norm rejects the extension.
9
+ */
10
+ export function manifest(declaration, component) {
11
+ const normalized = expand(declaration);
12
+ // `intervals` is defaulted by the schema, so the declaration is only whole after this
13
+ schemas.declaration.validate(normalized, 'Invalid cadence declaration');
14
+ for (const [endpoint, pulse] of Object.entries(normalized)) {
15
+ const operation = component.operations?.[endpoint];
16
+ if (operation === undefined)
17
+ throw new Error(`Pulse refers to undefined operation '${endpoint}'`);
18
+ if (!TYPES.has(operation.type))
19
+ throw new Error(`Pulse '${endpoint}' must refer to an operation of the allowed types: ` +
20
+ [...TYPES].join(', '));
21
+ if (pulse.intervals > pulse.cycle)
22
+ throw new Error(`Pulse '${endpoint}' splits a cycle of ${pulse.cycle} seconds into ` +
23
+ `${pulse.intervals} intervals, which is less than a second each`);
24
+ }
25
+ return normalized;
26
+ }
27
+ /** A pulse whose cycle is not split has nothing to state but the cycle. */
28
+ function expand(declaration) {
29
+ if (declaration === null || declaration === undefined)
30
+ return {};
31
+ return Object.fromEntries(Object.entries(declaration)
32
+ .map(([endpoint, pulse]) => [endpoint, typeof pulse === 'number' ? { cycle: pulse } : pulse]));
33
+ }
34
+ /**
35
+ * The types a receiver may refer to. An operation that produces no side effects has nothing to
36
+ * be called periodically for.
37
+ */
38
+ const TYPES = new Set(['transition', 'assignment', 'effect', 'unmanaged']);
39
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../source/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAIvC;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CAAE,WAA2C,EACnE,SAAmB;IACnB,MAAM,UAAU,GAA4B,MAAM,CAAC,WAAW,CAAC,CAAA;IAE/D,sFAAsF;IACtF,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAc,UAAU,EAAE,6BAA6B,CAAC,CAAA;IAEpF,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3D,MAAM,SAAS,GAAG,SAAS,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,CAAA;QAElD,IAAI,SAAS,KAAK,SAAS;YACzB,MAAM,IAAI,KAAK,CAAC,wCAAwC,QAAQ,GAAG,CAAC,CAAA;QAEtE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,UAAU,QAAQ,qDAAqD;gBACrF,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QAE1B,IAAI,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,KAAK;YAC/B,MAAM,IAAI,KAAK,CAAC,UAAU,QAAQ,uBAAuB,KAAK,CAAC,KAAK,gBAAgB;gBAClF,GAAG,KAAK,CAAC,SAAS,8CAA8C,CAAC,CAAA;IACvE,CAAC;IAED,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,2EAA2E;AAC3E,SAAS,MAAM,CAAE,WAA2C;IAC1D,IAAI,WAAW,KAAK,IAAI,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,EAAE,CAAA;IAEhE,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC;SAClD,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,CACzB,CAAC,QAAQ,EAAE,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACxE,CAAC;AAED;;;GAGG;AACH,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { Schema } from '@toa.io/schemas';
2
+ import type { Annotation, Declaration } from './types.js';
3
+ export declare const declaration: Schema<Declaration>;
4
+ export declare const annotation: Schema<Annotation>;
@@ -0,0 +1,7 @@
1
+ import { resolve } from 'node:path';
2
+ import * as schemas from '@toa.io/schemas';
3
+ const path = resolve(import.meta.dirname, '../schemas');
4
+ const namespace = schemas.namespace(path);
5
+ export const declaration = namespace.schema('declaration');
6
+ export const annotation = namespace.schema('annotation');
7
+ //# sourceMappingURL=schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../source/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAI1C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;AACvD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;AAEzC,MAAM,CAAC,MAAM,WAAW,GAAwB,SAAS,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;AAC/E,MAAM,CAAC,MAAM,UAAU,GAAuB,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA"}
@@ -0,0 +1,34 @@
1
+ /** What one component declares under `cadence:`, by the operation each one calls. */
2
+ export type Declaration = Record<string, Pulse>;
3
+ export interface Pulse {
4
+ /** seconds one whole cycle takes */
5
+ cycle: number;
6
+ /** intervals the cycle is split into; the gap between calls is `cycle / intervals` */
7
+ intervals: number;
8
+ }
9
+ /** `context.delay` — a call to be made later, answering the id that cancels it. */
10
+ export interface Delay {
11
+ (endpoint: string, request: object | null, options: Options): Promise<string>;
12
+ /** Raises where the id was never issued. */
13
+ cancel: (id: string) => Promise<void>;
14
+ }
15
+ export interface Options {
16
+ /** milliseconds from now */
17
+ interval: number;
18
+ /**
19
+ * Milliseconds the call may be late and still be made, or `null` for no bound at all.
20
+ *
21
+ * Stated rather than defaulted, because only the caller knows whether a late call is still
22
+ * the right call: an unpaid order expires on time or not at all, and a report is wanted
23
+ * whenever it can be had. Nothing else in the system can tell those apart.
24
+ *
25
+ * An upper bound on lateness, not a promise of timeliness. It is bounded below by how often
26
+ * a dispatcher scans, so a bound shorter than that is one nothing can honour.
27
+ */
28
+ overdue: number | null;
29
+ }
30
+ /** What an application states under `cadence:` in its context. */
31
+ export interface Annotation {
32
+ /** seconds between passes over the calls waiting to be made */
33
+ discreteness?: number;
34
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../source/types.ts"],"names":[],"mappings":""}
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./transpiled",
5
+ "rootDir": "./source"
6
+ },
7
+ "include": [
8
+ "source"
9
+ ],
10
+ "exclude": [
11
+ "**/*.test.ts"
12
+ ]
13
+ }