@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # [1.0.0-alpha.284](https://github.com/toa-io/toa/compare/v1.0.0-alpha.283...v1.0.0-alpha.284) (2026-09-05)
7
+
8
+ ### Features
9
+
10
+ * **cadence:** calls a component makes to itself, and calls it puts off ([8bbde5f](https://github.com/toa-io/toa/commit/8bbde5fc89e8e5549d5aa2bfcddb1beec0871565))
@@ -0,0 +1,72 @@
1
+ namespace: cadence
2
+ name: metronome
3
+
4
+ # The extension puts its dispatcher on this component, and nothing else here declares a cadence.
5
+ cadence: ~
6
+
7
+ entity:
8
+ schema:
9
+ type: object
10
+ properties:
11
+ lane:
12
+ description: Which dispatcher takes this row, as the outbox partitions its lanes
13
+ type: integer
14
+ due:
15
+ description: When the call is to be made, in milliseconds since the epoch
16
+ type: integer
17
+ expires:
18
+ description: >
19
+ After this the call is not made at all. A scan bounds on it, so a call nothing was
20
+ running to make in time is a row nobody reads, rather than an event anything sees.
21
+ type: integer
22
+ endpoint:
23
+ description: What to call, as `namespace.component.operation`
24
+ type: string
25
+ request:
26
+ description: The request to make, as it was handed over
27
+ type: object
28
+ required:
29
+ - lane
30
+ - due
31
+ - expires
32
+ - endpoint
33
+ index:
34
+ due:
35
+ lane: asc
36
+ due: asc
37
+
38
+ # `enumerate` reads what is due and `terminate` cancels one; both come from the prototype.
39
+ operations:
40
+ delay:
41
+ query: false
42
+ input:
43
+ type: object
44
+ properties:
45
+ endpoint:
46
+ type: string
47
+ pattern: ^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][_a-zA-Z0-9]*$
48
+ request:
49
+ type: object
50
+ interval:
51
+ description: Milliseconds from now
52
+ type: integer
53
+ minimum: 0
54
+ overdue:
55
+ description: >
56
+ Milliseconds the call may be late and still be made, or null for no bound. Stated
57
+ rather than defaulted: only the caller knows whether a late call is still the right
58
+ call.
59
+ type: integer
60
+ nullable: true
61
+ minimum: 0
62
+ required:
63
+ - endpoint
64
+ - interval
65
+ - overdue
66
+ output:
67
+ type: string
68
+
69
+ settle:
70
+ query: true
71
+ # a row may be cancelled while a page of them is being settled
72
+ concurrency: retry
@@ -0,0 +1,28 @@
1
+ import { LANES } from '@toa.io/extensions.cadence'
2
+
3
+ /**
4
+ * A call to make later. The request carries no query, so the transition is handed a new object.
5
+ *
6
+ * The lane is random, not one this replica owns. An outbox row is written into an owned lane so
7
+ * that in steady state a replica settles its own rows before it ever reads them — but a delayed
8
+ * row has no immediate path to settle, so nothing is gained, and an even spread is what the
9
+ * dispatchers want.
10
+ */
11
+ export function transition (input, object) {
12
+ const due = Date.now() + input.interval
13
+
14
+ object.lane = Math.floor(Math.random() * LANES)
15
+ object.due = due
16
+ object.endpoint = input.endpoint
17
+
18
+ // absolute, because the bound is the caller's and a scan reads rows of many callers at once.
19
+ // No bound is the end of representable time rather than an absent field, so that one
20
+ // comparison answers for every row
21
+ object.expires = input.overdue === null ? Number.MAX_SAFE_INTEGER : due + input.overdue
22
+
23
+ // a call that takes no request has none: the entity's `request` is an object where it is
24
+ // there at all, and a null would not fit it
25
+ if (input.request !== undefined) object.request = input.request
26
+
27
+ return object.id
28
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The rows whose calls have been made. They are tombstoned rather than removed: the record of
3
+ * what was asked for outlives the call, and a query answers only what is still owed.
4
+ */
5
+ export function transition (_, objects) {
6
+ const now = Date.now()
7
+
8
+ for (const object of objects) object._deleted = now
9
+ }
@@ -0,0 +1,3 @@
1
+ export * from './toa.d.ts'
2
+
3
+ // What a manifest does not state belongs here, and every run keeps it.
@@ -0,0 +1,45 @@
1
+ // Written by `toa types`. Every run rewrites it.
2
+ // What a manifest does not state belongs in a file of your own.
3
+
4
+ import type { Query } from '@toa.io/core'
5
+ import type { Readable } from 'node:stream'
6
+
7
+ export interface Entity {
8
+ /** Which dispatcher takes this row, as the outbox partitions its lanes */
9
+ lane: number
10
+ /** When the call is to be made, in milliseconds since the epoch */
11
+ due: number
12
+ /** After this the call is not made at all. A scan bounds on it, so a call nothing was running to make in time is a row nobody reads, rather than an event anything sees. */
13
+ expires: number
14
+ /** What to call, as `namespace.component.operation` */
15
+ endpoint: string
16
+ /** The request to make, as it was handed over */
17
+ request?: Record<string, unknown>
18
+ id: string
19
+ _version?: number
20
+ _created?: number
21
+ _updated?: number
22
+ _deleted?: number | null
23
+ }
24
+
25
+ export type DelayInput = {
26
+ endpoint: string
27
+ request?: Record<string, unknown>
28
+ /** Milliseconds from now */
29
+ interval: number
30
+ /** Milliseconds the call may be late and still be made, or null for no bound. Stated rather than defaulted: only the caller knows whether a late call is still the right call. */
31
+ overdue: number | null
32
+ }
33
+
34
+ export type DelayOutput = string
35
+
36
+ export interface Component {
37
+ delay: (request: { input: DelayInput, task?: boolean }) => Promise<DelayOutput>
38
+ settle: (request: { input?: null, query: Query<Entity>, task?: boolean }) => Promise<unknown>
39
+ assign: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity>
40
+ ensure: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity>
41
+ enumerate: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity[]>
42
+ observe: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity | null>
43
+ stream: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Readable>
44
+ terminate: (request: { input?: null, query?: Query<Entity>, task?: boolean }) => Promise<Entity>
45
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@toa.io/extensions.cadence",
3
+ "version": "1.0.0-alpha.284",
4
+ "type": "module",
5
+ "description": "Toa Schedule",
6
+ "author": "temich <tema.gurtovoy@gmail.com>",
7
+ "homepage": "https://github.com/toa-io/toa#readme",
8
+ "main": "transpiled/index.js",
9
+ "types": "transpiled/index.d.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/toa-io/toa.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/toa-io/toa/issues"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "dependencies": {
21
+ "@toa.io/core": "1.0.0-alpha.284",
22
+ "@toa.io/schemas": "1.0.0-alpha.283",
23
+ "openspan": "1.0.0-alpha.277"
24
+ },
25
+ "scripts": {
26
+ "transpile": "tsc"
27
+ }
28
+ }
package/readme.md ADDED
@@ -0,0 +1,188 @@
1
+ # Toa Cadence
2
+
3
+ Calls a component makes to itself, and calls it puts off.
4
+
5
+ A **pulse** calls an operation of its own component on a cadence its manifest states. A **delay**
6
+ hands one call over to be made later.
7
+
8
+ Both rest on [`atomicity`](/connectors/atomicity), which is what keeps two replicas from making
9
+ one call and where that promise is written down. Where it is not configured, nothing is called
10
+ at all.
11
+
12
+ ## Pulse
13
+
14
+ ```yaml
15
+ # manifest.toa.yaml
16
+ cadence:
17
+ sweep: # an operation of this component
18
+ cycle: 86400 # seconds one whole cycle takes
19
+ intervals: 24 # what it is split into, so one call an hour
20
+ ```
21
+
22
+ The gap between calls is `cycle / intervals`, derived rather than declared, so the two cannot
23
+ disagree. `intervals` defaults to `1`, one call per cycle, which is also what the shorthand
24
+ declares:
25
+
26
+ ```yaml
27
+ # manifest.toa.yaml
28
+ cadence:
29
+ sweep: 3600 # once an hour
30
+ ```
31
+
32
+ The operation is called with the cycle and the interval as its input:
33
+
34
+ ```javascript
35
+ async function sweep ({ n, i }, context) { }
36
+ ```
37
+
38
+ `n` is `intervals` and `i` is which of them this call is for, from `0` to `n - 1`. They are there
39
+ so that one sweep can be spread across its cycle rather than run whole every time: an operation
40
+ that takes `i` as its share of the work does a twenty-fourth of it every hour instead of all of
41
+ it at midnight.
42
+
43
+ A pulse may name any operation a receiver may — `transition`, `assignment`, `effect` or
44
+ `unmanaged`. An operation that produces no side effects has nothing to be called periodically
45
+ for.
46
+
47
+ A cycle is measured from the Unix epoch rather than from a calendar, so `86400` means UTC
48
+ midnight and `3600` the top of the hour. A cycle that is not a divisor of a day lands on a
49
+ boundary that is not any particular time of day. Time zones and calendar months are not
50
+ supported.
51
+
52
+ ### What to expect
53
+
54
+ **An interval may not be called.** A rollout, a crash, or an operation that raised or ran past
55
+ its own interval each cost that interval, and nothing makes it up afterwards.
56
+
57
+ **So select by state, not by position.** With `intervals` above `1` the shares are a partition —
58
+ `i = 5` missed is not covered by `i = 6`, and waits a whole cycle. Write the operation to take
59
+ what in its share is still due, rather than everything in its share, and a missed interval costs
60
+ a delay instead of a day. An operation written the other way loses a day's work to a
61
+ thirty-second rollout.
62
+
63
+ Where a call has to happen even if nothing was running when it came due, it needs a stored
64
+ schedule: [`delay`](#delay) is one.
65
+
66
+ **An interval is called once.** Two calls for one interval need the clocks of two machines to
67
+ disagree and the work to change hands in the same moment, which is a window the width of that
68
+ disagreement.
69
+
70
+ **A call is never made while the one before it is still running.** If the work does not fit the
71
+ gap the interval is skipped and says so; where that is logged, the cycle is too short or has too
72
+ few intervals.
73
+
74
+ **An operation that raises loses its interval, and nothing is retried.** Skipping is a choice
75
+ rather than a conclusion, and there is no setting for it — retrying would be as defensible, and
76
+ either answer put to everyone who wanted an operation called every hour is a worse cost than
77
+ picking one.
78
+
79
+ `intervals` is not a replica count. With `intervals: 24` and three replicas each makes eight of
80
+ the day's calls: the cycle is spread over *time*, not over the fleet. Replicas beyond `intervals`
81
+ make none.
82
+
83
+ ## Delay
84
+
85
+ ```javascript
86
+ const id = await context.delay('mail.sender.remind', { input: { user } },
87
+ { interval: 7 * 24 * 3600 * 1000, overdue: null })
88
+
89
+ await context.delay.cancel(id)
90
+ ```
91
+
92
+ | | |
93
+ |---|---|
94
+ | `interval` | milliseconds from now |
95
+ | `overdue` | milliseconds the call may be late and still be made, or `null` for no bound |
96
+
97
+ The call is made once the delay has passed, and waits for the target where it is not there to
98
+ take it. The id it answers cancels it, and `cancel` raises where the id was never issued.
99
+
100
+ Nothing is declared for it beyond naming the extension. A component that only delays calls says
101
+ so with an empty declaration:
102
+
103
+ ```yaml
104
+ # manifest.toa.yaml
105
+ cadence: ~
106
+ ```
107
+
108
+ ### How late is too late
109
+
110
+ `overdue` bounds lateness, and lateness is mostly what a failure causes rather than what the
111
+ ordinary path has: a call is made at the moment it is due. How much is left of that in practice
112
+ depends on how coarsely the calls are looked over — see [Discreteness](#discreteness).
113
+
114
+ | | |
115
+ |---|---|
116
+ | `null` | no bound: made whenever it can be, however late that is |
117
+ | `0` | on time or never |
118
+ | a number | made if no more than that many milliseconds late |
119
+
120
+ **There is no default, and it is stated on every call.** Only the caller knows whether a late
121
+ call is still the right call — an unpaid order expires on time or not at all, a report is wanted
122
+ whenever it can be had — and nothing else in the system can tell those apart. A default would
123
+ answer for it, and either answer is wrong for somebody: the wrong `null` makes a stale call, and
124
+ the wrong number drops a call and says nothing.
125
+
126
+ ### Discreteness
127
+
128
+ Calls waiting to be made are looked over on a period — `discreteness`, a minute by default — and
129
+ everything here is measured against it.
130
+
131
+ **A delay shorter than `discreteness` is served late.** It is made at the next pass rather than
132
+ at the moment it was due, so a five second delay under a one minute `discreteness` lands up to a
133
+ minute after it was asked for. A delay longer than `discreteness` is made on time, because the
134
+ pass before it has already set it going.
135
+
136
+ **An `overdue` shorter than `discreteness` can be missed where nothing is wrong**, and combined
137
+ with a delay shorter than it will almost always be: the pass that would have made the call finds
138
+ it already past its bound and leaves it. State an `overdue` several times `discreteness`, or
139
+ `null`.
140
+
141
+ **A cancellation is reliable while the call is further out than `discreteness`**, and is a race
142
+ closer than that, where the call may already be on its way.
143
+
144
+ So `discreteness` and `overdue` are chosen together against what the application needs. A call
145
+ that must be made within seconds of its time, or cancellable within seconds of it, needs a
146
+ `discreteness` of seconds — at the cost of looking the calls over that much more often.
147
+
148
+ ### What to expect
149
+
150
+ **A call may be made more than once**, so where a second one would cost something, the target
151
+ has to be the one that refuses it.
152
+
153
+ **A call may not be made at all**, in four ways:
154
+
155
+ - **the caller crashed** between committing its own change and handing the call over. This is
156
+ not transactional with your state, and nothing can tell — where the semantics allow it, hand
157
+ the call over first;
158
+ - **it expired**, having been given an `overdue` and nothing having been able to make it in
159
+ time;
160
+ - **it was cancelled** — which is reliable while the call is further out than `discreteness`,
161
+ and a race closer than that;
162
+ - **it could not be made.** One attempt is given, so a call that could not go out is reported
163
+ and dropped rather than tried again — the same choice a pulse makes, for the same reason.
164
+
165
+ What is reported here is a call that could not be made at all. An exception the operation itself
166
+ raises belongs to the component that raised it and is reported there.
167
+
168
+ A pulse trades the other way: its receiver selects by state, so a skipped interval heals itself
169
+ and a second call is the expensive failure.
170
+
171
+ ### The horizon
172
+
173
+ A horizon of years costs nothing, and a call put off that long is worth no more than the target
174
+ that will be there to take it: the target may be gone by the time it comes due, and its input
175
+ may no longer be one it accepts. Either way the attempt is spent.
176
+
177
+ ## Configuration
178
+
179
+ ```yaml
180
+ # context.toa.yaml
181
+ cadence:
182
+ discreteness: 60 # seconds between passes over the calls waiting to be made
183
+ ```
184
+
185
+ `discreteness` is how often the calls waiting to be made are looked over. It defaults to 60,
186
+ which is what an application that states nothing gets. It is the floor under how closely a
187
+ delayed call can be cancelled and how small an `overdue` is worth stating — see
188
+ [Discreteness](#discreteness) — and lowering it costs a pass that runs more often.
@@ -0,0 +1,9 @@
1
+ # context.toa.yaml cadence annotation
2
+ type: object
3
+ nullable: true
4
+ properties:
5
+ discreteness:
6
+ description: Seconds between passes over the calls waiting to be made
7
+ type: integer
8
+ minimum: 1
9
+ additionalProperties: false
@@ -0,0 +1,21 @@
1
+ # manifest.toa.yaml cadence declaration: the operations called on a cadence
2
+ type: object
3
+ propertyNames:
4
+ pattern: ^[a-zA-Z]+([_a-zA-Z0-9]*[a-zA-Z0-9]+)?$
5
+ patternProperties:
6
+ ".*":
7
+ type: object
8
+ properties:
9
+ cycle:
10
+ description: Seconds one whole cycle takes
11
+ type: integer
12
+ minimum: 1
13
+ intervals:
14
+ description: >
15
+ Intervals the cycle is split into. The gap between calls is `cycle / intervals`,
16
+ and the operation is told which interval it is running for.
17
+ type: integer
18
+ minimum: 1
19
+ default: 1
20
+ required: [cycle]
21
+ additionalProperties: false
@@ -0,0 +1,64 @@
1
+ import { Connector } from '@toa.io/core'
2
+ import type { Local } from './Local.js'
3
+ import type { Options } from './types.js'
4
+
5
+ interface Input {
6
+ endpoint: string
7
+ interval: number
8
+ overdue: number | null
9
+ request?: object
10
+ }
11
+
12
+ /**
13
+ * `context.delay(endpoint, request, delay)` — a call to be made later, handed to the component
14
+ * that keeps them.
15
+ *
16
+ * It is not transactional with the caller's state. A crash between committing a change and
17
+ * handing over the call it should have scheduled loses the call, and there is no way for this
18
+ * to know. Where the semantics allow it, hand the call over first.
19
+ *
20
+ * @implements {toa.core.extensions.Aspect}
21
+ */
22
+ export class Aspect extends Connector {
23
+ public readonly name = 'delay'
24
+
25
+ private readonly metronome: Local
26
+
27
+ public constructor (metronome: Local) {
28
+ super()
29
+
30
+ this.metronome = metronome
31
+
32
+ this.depends(metronome)
33
+ }
34
+
35
+ // named rather than forwarded, so that the connector's lifecycle is not reachable from an
36
+ // algorithm — the same reason the atom aspect names its own
37
+ public async invoke (method: string, ...args: any[]): Promise<unknown> {
38
+ switch (method) {
39
+ case 'delay': return await this.delay(args[0], args[1], args[2])
40
+ case 'cancel': return await this.cancel(args[0])
41
+ default: throw new Error(`Delay aspect has no '${method}' method`)
42
+ }
43
+ }
44
+
45
+ /** Answers the id the call was given, which is what cancels it. */
46
+ private async delay (endpoint: string, request: object | null,
47
+ options: Options): Promise<unknown> {
48
+ const input: Input = { endpoint, interval: options.interval, overdue: options.overdue }
49
+
50
+ // a call that takes no request carries none, rather than a null one
51
+ if (request !== null && request !== undefined) input.request = request
52
+
53
+ return await this.metronome.invoke('delay', { input })
54
+ }
55
+
56
+ /**
57
+ * Tombstones the row, which is the prototype's `terminate`. An id that was never issued has
58
+ * nothing to terminate and raises; one whose call has already been made has a row still, and
59
+ * terminating it again is not an error.
60
+ */
61
+ private async cancel (id: string): Promise<unknown> {
62
+ return await this.metronome.invoke('terminate', { query: { id }, input: null })
63
+ }
64
+ }
@@ -0,0 +1,47 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import { Connector } from '@toa.io/core'
4
+ import type { Dirent } from 'node:fs'
5
+ import type { Host } from './Factory.js'
6
+
7
+ /** The components this extension ships, which run inside its own service. */
8
+ export function components (): Components {
9
+ const labels: string[] = []
10
+ const paths: string[] = []
11
+
12
+ for (const entry of entries()) {
13
+ labels.push(entry.name.replace('.', '-'))
14
+ paths.push(resolve(ROOT, entry.name))
15
+ }
16
+
17
+ return { labels, paths }
18
+ }
19
+
20
+ export class Composition extends Connector {
21
+ private readonly host: Host
22
+
23
+ public constructor (host: Host) {
24
+ super()
25
+
26
+ this.host = host
27
+ }
28
+
29
+ protected override async open (): Promise<void> {
30
+ const composition = await this.host.composition(components().paths)
31
+
32
+ await composition.connect()
33
+
34
+ this.depends(composition)
35
+ }
36
+ }
37
+
38
+ function entries (): Dirent[] {
39
+ return readdirSync(ROOT, { withFileTypes: true }).filter((entry) => entry.isDirectory())
40
+ }
41
+
42
+ interface Components {
43
+ labels: string[]
44
+ paths: string[]
45
+ }
46
+
47
+ const ROOT = resolve(import.meta.dirname, '../components/')