@toa.io/core 1.0.0-alpha.292 → 1.0.0-alpha.299

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 (138) hide show
  1. package/package.json +10 -4
  2. package/transpiled/call.js +12 -0
  3. package/transpiled/call.js.map +1 -1
  4. package/transpiled/component.js +31 -4
  5. package/transpiled/component.js.map +1 -1
  6. package/transpiled/context.d.ts +5 -0
  7. package/transpiled/context.js +6 -0
  8. package/transpiled/context.js.map +1 -1
  9. package/transpiled/emission.d.ts +4 -2
  10. package/transpiled/emission.js +4 -2
  11. package/transpiled/emission.js.map +1 -1
  12. package/transpiled/entities/entity.js +34 -6
  13. package/transpiled/entities/entity.js.map +1 -1
  14. package/transpiled/entities/factory.d.ts +5 -0
  15. package/transpiled/entities/factory.js +6 -2
  16. package/transpiled/entities/factory.js.map +1 -1
  17. package/transpiled/event.d.ts +2 -2
  18. package/transpiled/event.js +6 -1
  19. package/transpiled/event.js.map +1 -1
  20. package/transpiled/exceptions.d.ts +19 -0
  21. package/transpiled/exceptions.js +74 -2
  22. package/transpiled/exceptions.js.map +1 -1
  23. package/transpiled/index.d.ts +2 -1
  24. package/transpiled/index.js +2 -1
  25. package/transpiled/index.js.map +1 -1
  26. package/transpiled/{outbox/outbox.d.ts → outbox.d.ts} +6 -7
  27. package/transpiled/outbox.js +424 -0
  28. package/transpiled/outbox.js.map +1 -0
  29. package/transpiled/query/options.js +1 -1
  30. package/transpiled/query/options.js.map +1 -1
  31. package/transpiled/receiver.js +14 -1
  32. package/transpiled/receiver.js.map +1 -1
  33. package/transpiled/remote.js +2 -3
  34. package/transpiled/remote.js.map +1 -1
  35. package/transpiled/state.d.ts +1 -1
  36. package/transpiled/trail.d.ts +49 -0
  37. package/transpiled/trail.js +98 -0
  38. package/transpiled/trail.js.map +1 -0
  39. package/transpiled/types/bindings.d.ts +22 -0
  40. package/transpiled/types/extensions.d.ts +13 -2
  41. package/transpiled/types/message.d.ts +5 -0
  42. package/transpiled/types/outbox.d.ts +29 -3
  43. package/transpiled/types/request.d.ts +5 -0
  44. package/transpiled/types/storages.d.ts +20 -0
  45. package/CHANGELOG.md +0 -43
  46. package/source/assignment.ts +0 -27
  47. package/source/call.ts +0 -63
  48. package/source/cascade.ts +0 -32
  49. package/source/component.ts +0 -83
  50. package/source/composition.ts +0 -27
  51. package/source/connector.ts +0 -217
  52. package/source/context.ts +0 -68
  53. package/source/contract/contract.ts +0 -23
  54. package/source/contract/index.ts +0 -2
  55. package/source/contract/reply.ts +0 -56
  56. package/source/contract/request.ts +0 -140
  57. package/source/contract/schemas.ts +0 -39
  58. package/source/discovery.ts +0 -53
  59. package/source/effect.ts +0 -14
  60. package/source/emission.ts +0 -21
  61. package/source/entities/changeset.ts +0 -37
  62. package/source/entities/entity.ts +0 -137
  63. package/source/entities/factory.ts +0 -52
  64. package/source/entities/index.ts +0 -4
  65. package/source/entities/newid.ts +0 -9
  66. package/source/entities/set.ts +0 -27
  67. package/source/event.ts +0 -57
  68. package/source/exceptions.ts +0 -133
  69. package/source/exposition.ts +0 -33
  70. package/source/guard.ts +0 -16
  71. package/source/index.ts +0 -36
  72. package/source/locator.ts +0 -37
  73. package/source/observation.ts +0 -16
  74. package/source/operation.ts +0 -139
  75. package/source/outbox/index.ts +0 -1
  76. package/source/outbox/outbox.ts +0 -341
  77. package/source/query/criteria.ts +0 -90
  78. package/source/query/options.ts +0 -35
  79. package/source/query.ts +0 -67
  80. package/source/receiver.ts +0 -120
  81. package/source/reflection.ts +0 -20
  82. package/source/remote.ts +0 -20
  83. package/source/state.ts +0 -179
  84. package/source/transition.ts +0 -79
  85. package/source/transmission.ts +0 -38
  86. package/source/types/atomicity.ts +0 -63
  87. package/source/types/bindings.ts +0 -59
  88. package/source/types/bridges.ts +0 -54
  89. package/source/types/extensions.ts +0 -89
  90. package/source/types/index.ts +0 -14
  91. package/source/types/message.ts +0 -5
  92. package/source/types/operations.ts +0 -31
  93. package/source/types/outbox.ts +0 -35
  94. package/source/types/receiver.ts +0 -10
  95. package/source/types/request.ts +0 -54
  96. package/source/types/state.ts +0 -9
  97. package/source/types/storages.ts +0 -134
  98. package/source/unmanaged.ts +0 -8
  99. package/test/call.fixtures.js +0 -25
  100. package/test/call.test.js +0 -79
  101. package/test/component.fixtures.js +0 -18
  102. package/test/component.test.js +0 -57
  103. package/test/connector.fixtures.js +0 -56
  104. package/test/connector.test.js +0 -266
  105. package/test/context.fixtures.js +0 -14
  106. package/test/context.test.js +0 -40
  107. package/test/contract/conditions.test.js +0 -32
  108. package/test/contract/contract.fixtures.js +0 -28
  109. package/test/contract/request.test.js +0 -196
  110. package/test/discovery.test.js +0 -71
  111. package/test/emission.fixtures.js +0 -13
  112. package/test/emission.test.js +0 -60
  113. package/test/entities/entity.fixtures.js +0 -24
  114. package/test/entities/entity.test.js +0 -137
  115. package/test/entities/factory.fixtures.js +0 -31
  116. package/test/entities/factory.test.js +0 -99
  117. package/test/entities/set.fixtures.js +0 -9
  118. package/test/entities/set.test.js +0 -13
  119. package/test/event.fixtures.js +0 -24
  120. package/test/event.test.js +0 -162
  121. package/test/locator.test.js +0 -120
  122. package/test/outbox.test.js +0 -202
  123. package/test/query.fixtures.js +0 -96
  124. package/test/query.test.js +0 -183
  125. package/test/receiver.fixtures.js +0 -20
  126. package/test/receiver.test.js +0 -144
  127. package/test/reflection.test.js +0 -31
  128. package/test/state.fixtures.js +0 -49
  129. package/test/state.test.js +0 -162
  130. package/test/transmission.fixtures.js +0 -13
  131. package/test/transmission.test.js +0 -77
  132. package/transpiled/outbox/index.d.ts +0 -1
  133. package/transpiled/outbox/index.js +0 -2
  134. package/transpiled/outbox/index.js.map +0 -1
  135. package/transpiled/outbox/outbox.js +0 -268
  136. package/transpiled/outbox/outbox.js.map +0 -1
  137. package/tsconfig.json +0 -10
  138. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,98 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { environment } from '@toa.io/generic';
3
+ import { LoopException } from './exceptions.js';
4
+ /**
5
+ * The chain of hops the invocation running now passed through, and the rule that refuses one
6
+ * that has come back to where it had been.
7
+ *
8
+ * Ambient, like the trace context and for the same reason: an algorithm's `context` is built
9
+ * once per operation at boot and shared by every invocation of it, so nothing per-invocation
10
+ * can be handed to it. It is not the trace context's own store, which `console.span` replaces
11
+ * on every span and does not enter at all when the trace is unsampled — a chain that
12
+ * disappears under sampling is a breaker that stops breaking in production.
13
+ *
14
+ * One writer, `Component.invoke`, where the hop is appended; two readers, `Call.invoke`, which
15
+ * puts it on the request it is about to send, and `Outbox.row`, which writes it onto the row so
16
+ * it outlives the operation that caused it.
17
+ */
18
+ // as openspan holds its own: a process may carry two copies of this module, and a chain that
19
+ // is empty because the writer sat in the other one is a breaker that never fires
20
+ const KEY = Symbol.for('toa.core.trail');
21
+ const storage = (globalThis[KEY] ??= new AsyncLocalStorage());
22
+ /**
23
+ * What the environment says, read where a component is built rather than once for the process,
24
+ * as the outbox reads its own — so a composition booted after a variable was set sees it.
25
+ *
26
+ * `TOA_TRAIL_REPEATS=0` refuses nothing: the chain is still stamped, carried and bounded. It is
27
+ * the off switch, and it exists because this refuses calls an application may be making today —
28
+ * a handshake written as `a > b > a > b > a` is three occurrences of one hop. A breaker with no
29
+ * way to open it is itself the outage.
30
+ */
31
+ export function limits() {
32
+ return {
33
+ repeats: number('TOA_TRAIL_REPEATS', 3),
34
+ depth: number('TOA_TRAIL_DEPTH', 32)
35
+ };
36
+ }
37
+ /** The chain that led to the invocation running now, where there is one. */
38
+ export function current() {
39
+ return storage.getStore();
40
+ }
41
+ /** Runs `task` as the hop the chain now ends with. */
42
+ export async function follow(hops, task) {
43
+ return storage.run(hops, task);
44
+ }
45
+ /**
46
+ * The chain this hop makes, or a raise where it is one hop too many: a hop already taken
47
+ * `repeats` times is a cycle, and a chain past `depth` is one nothing meant to make. Both are
48
+ * permanent, so what hits one is set aside rather than tried again into the same loop.
49
+ *
50
+ * The chain is copied rather than appended to, which is what makes it a path down the call
51
+ * tree rather than a log of everything that happened — an operation calling one endpoint fifty
52
+ * times makes fifty chains of one hop, not one chain of fifty.
53
+ */
54
+ export function extend(inbound, hop, limits) {
55
+ const hops = clip(received(inbound), limits.depth);
56
+ const trail = [...hops, hop];
57
+ if (limits.repeats === 0)
58
+ return trail;
59
+ if (trail.length > limits.depth)
60
+ throw new LoopException(`Call chain is ${trail.length} hops deep`, trail);
61
+ let seen = 0;
62
+ for (const passed of hops)
63
+ if (passed === hop)
64
+ seen++;
65
+ if (seen + 1 >= limits.repeats)
66
+ throw new LoopException(`'${hop}' is hop ${seen + 1} of this chain`, trail);
67
+ return trail;
68
+ }
69
+ /**
70
+ * How an event is named in a chain. The sigil is what tells it from an operation: the two are
71
+ * the same shape, a component may declare one of each under a single name, and `sync` is both
72
+ * the event every component inherits and an ordinary name for an operation.
73
+ *
74
+ * What an operator rewires to break a cycle is the subscription rather than the operation, so a
75
+ * chain that named only operations would not say how a component was re-entered.
76
+ */
77
+ export function event(destination) {
78
+ return '~' + destination;
79
+ }
80
+ /**
81
+ * What came off the wire, as a chain and nothing else. Neither the request contract nor a
82
+ * message validates this — a request is not validated at all once it is `authentic` — so a
83
+ * malformed one would otherwise become a `TypeError` where a named exception was contracted
84
+ * for. Bounding it is `extend`'s, one hop later, which is the only place the limits are known.
85
+ */
86
+ export function received(value) {
87
+ return Array.isArray(value) ? value.filter((hop) => typeof hop === 'string') : [];
88
+ }
89
+ // with the hop being appended that is one past the cap, which is enough to be refused; the
90
+ // rest is a message buying memory, and where the rule is off it is what bounds the chain
91
+ function clip(hops, depth) {
92
+ return hops.length > depth ? hops.slice(0, depth) : hops;
93
+ }
94
+ function number(variable, fallback) {
95
+ const declared = Number(environment.get(variable));
96
+ return Number.isNaN(declared) || declared < 0 ? fallback : declared;
97
+ }
98
+ //# sourceMappingURL=trail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trail.js","sourceRoot":"","sources":["../source/trail.ts"],"names":[],"mappings":"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"}
@@ -39,4 +39,26 @@ export interface Factory {
39
39
  /** `group` is absent for an exclusive subscription */
40
40
  receiver?(locator: Locator, label: string, group: string | undefined, receiver: Receiver): Connector;
41
41
  broadcast?(name: string, group?: string): Broadcast;
42
+ /**
43
+ * A channel is a name the caller picks, from which the binding derives whatever its
44
+ * transport needs, and `uris` are the brokers to carry it over — so a second broker set
45
+ * costs the binding no configuration and no variable of its own.
46
+ */
47
+ outbound?(channel: string, uris: string[]): Outbound;
48
+ /** what arrives on `channel` under `label` */
49
+ inbound?(channel: string, uris: string[], label: string, sink: Inbound): Connector;
50
+ }
51
+ /**
52
+ * Publishes to a channel, addressed by label. It forwards messages and nothing else: what
53
+ * `send` is handed is what is published, with no envelope, no field and no header of the
54
+ * binding's own, and none stripped — because a message shape is often somebody else's
55
+ * contract, and an extension shipping changes into another system must be able to send
56
+ * exactly what that system accepts.
57
+ */
58
+ export interface Outbound extends Connector {
59
+ send(label: string, message: object): Promise<void>;
60
+ }
61
+ /** What a binding hands a message to. */
62
+ export interface Inbound {
63
+ accept(message: object): Promise<void>;
42
64
  }
@@ -5,9 +5,10 @@ import type { Remote } from '../remote.js';
5
5
  import type { Receiver } from './receiver.js';
6
6
  import type { Context } from '../context.js';
7
7
  import type { Storage } from './storages.js';
8
- import type { Broadcast, Emitter } from './bindings.js';
8
+ import type { Broadcast, Emitter, Inbound, Outbound } from './bindings.js';
9
9
  import type { Atom } from './atomicity.js';
10
10
  import type { Source } from './request.js';
11
+ import type { Destination } from './outbox.js';
11
12
  /**
12
13
  * What the process hosting an extension provides to it: the counterpart of a component's
13
14
  * context. What is returned is a connector the extension depends on.
@@ -23,6 +24,10 @@ export interface Host {
23
24
  receive(label: string, receiver: Receiver): Promise<Connector>;
24
25
  /** what the replicas of one group decide together */
25
26
  atom(group: string): Atom;
27
+ /** where this deployment publishes a channel, over the brokers named */
28
+ outbound(binding: string, channel: string, uris: string[]): Promise<Outbound>;
29
+ /** what arrives on a channel under one label */
30
+ inbound(binding: string, channel: string, uris: string[], label: string, sink: Inbound): Promise<Connector>;
26
31
  }
27
32
  /**
28
33
  * `Manifest` is a type parameter rather than an import: `@toa.io/norm` depends on core, so
@@ -31,12 +36,18 @@ export interface Host {
31
36
  export interface Factory<Manifest = unknown> {
32
37
  tenant?(locator: Locator, declaration: any, manifest: Manifest): Connector | Promise<Connector>;
33
38
  aspect?(locator: Locator, declaration: any): Aspect | Aspect[];
39
+ /**
40
+ * Where a committed state change of this component goes, beside its own events. Read before
41
+ * the storage is made, because one of these is what gives a component an outbox when it
42
+ * declares no event at all.
43
+ */
44
+ destination?(locator: Locator, declaration: any, manifest: Manifest): Destination | Promise<Destination> | undefined;
34
45
  /** what the extension runs as a process of its own; `null` where it is off here */
35
46
  service?(): Connector | null | Promise<Connector | null>;
36
47
  component?(component: Component): Component;
37
48
  context?(context: Context): Context;
38
49
  manage?(composition: Connector): Connector;
39
- storage?(storage: Storage): Storage;
50
+ storage?(storage: Storage, locator: Locator): Storage;
40
51
  emitter?(emitter: Emitter, label: string, locator: Locator): Emitter;
41
52
  receiver?(receiver: Receiver, locator: Locator): Receiver;
42
53
  }
@@ -2,4 +2,9 @@ export interface Message<T = any> {
2
2
  payload: T;
3
3
  /** W3C traceparent */
4
4
  telemetry?: string;
5
+ /**
6
+ * The hops that led to the state change this is about, so a receiver of it continues the
7
+ * chain rather than starting one. See `core/source/trail.ts`.
8
+ */
9
+ trail?: string[];
5
10
  }
@@ -1,3 +1,4 @@
1
+ import type { Connector } from '../connector.js';
1
2
  import type { Event } from './state.js';
2
3
  /**
3
4
  * The intent to publish, committed with the state change it belongs to. Everything about it
@@ -8,21 +9,46 @@ export interface Row {
8
9
  id: string;
9
10
  /** which replica pumps this row; carries no other meaning, and no ordering */
10
11
  lane: number;
12
+ /** settled for every destination */
11
13
  published: boolean;
12
14
  /** not before this */
13
15
  pending: number;
16
+ /** the destinations it has not been sent to yet, by name */
17
+ outstanding: string[];
18
+ /**
19
+ * The hops that led to the change. Written onto the row rather than left in scope, because
20
+ * the pump publishes off the operation's path — possibly in another replica, an hour later.
21
+ * Absent on a row written before this existed, which reads as a chain that starts there.
22
+ */
23
+ trail?: string[];
14
24
  /** an assignment's images are absent until the storage fills them in */
15
25
  event: Event;
16
26
  }
27
+ /**
28
+ * Somewhere a committed state change goes. A component's `Emission` is one, under the name
29
+ * `events`; an extension may contribute others, and each is published and settled on its own,
30
+ * so one that is down delays nothing but itself.
31
+ */
32
+ export interface Destination extends Connector {
33
+ /** what a row is outstanding for, as the row records it */
34
+ readonly name: string;
35
+ /** the row, not its event: what goes on the wire is the chain it carries too */
36
+ emit(row: Row): Promise<void>;
37
+ }
17
38
  /**
18
39
  * The read side of an outbox. What writes a row is the storage's own: it happens inside the
19
40
  * transaction the storage opened, which core never reaches into.
20
41
  */
21
42
  export interface Storage {
22
43
  /**
23
- * One page of what is due, still unpublished, and in one of the given lanes, in the order
24
- * the rows were written. `after` continues from the last id of the page before.
44
+ * One page of what is due, not settled for every destination, and in one of the given lanes,
45
+ * in the order the rows were written. `after` continues from the last id of the page before.
25
46
  */
26
47
  pending(lanes: number[], now: number, limit: number, after?: string): Promise<Row[]>;
27
- settle(ids: string[]): Promise<void>;
48
+ /**
49
+ * Takes `destinations` out of what those rows are outstanding for, and marks published the
50
+ * ones left outstanding for nothing. Several destinations at once, because the ordinary case
51
+ * is all of them landing in one window, and that is then one write.
52
+ */
53
+ settle(ids: string[], destinations: string[]): Promise<void>;
28
54
  }
@@ -38,6 +38,11 @@ export interface Request<Input = any, Entity = any> {
38
38
  /** W3C traceparent */
39
39
  telemetry?: string;
40
40
  source?: Source;
41
+ /**
42
+ * The hops this call passed through, oldest first. Stamped by the framework; a call that has
43
+ * been where it is going already is refused rather than made. See `core/source/trail.ts`.
44
+ */
45
+ trail?: string[];
41
46
  }
42
47
  /**
43
48
  * An error an operation declares and returns. A call resolves to it rather than throwing:
@@ -19,6 +19,8 @@ export interface Record {
19
19
  UPDATED?: number;
20
20
  /** a tombstone's timestamp; `null` on a live record */
21
21
  DELETED?: number | null;
22
+ /** the rank of the region that last wrote it; `0` where there is one region */
23
+ REGION?: number;
22
24
  [key: string]: any;
23
25
  }
24
26
  /** Everything a request query carried that was not a selector. */
@@ -81,6 +83,24 @@ export interface Storage extends Connector {
81
83
  * starting with a structure nothing has made.
82
84
  */
83
85
  readonly migrates?: boolean;
86
+ /**
87
+ * Writes `record` as it stands — its `VERSION`, its timestamps, its `REGION` and whatever
88
+ * else it carries — where what it would replace precedes it: a lower `VERSION`, or the same
89
+ * `VERSION` written by a region this one outranks. `false` where it does not: nothing is
90
+ * written, and that is not an error.
91
+ *
92
+ * Both sides of the comparison are on the two records, so this takes nothing else. What it
93
+ * is for is a record that was written somewhere else and has to land here as it was, which
94
+ * is neither a transition nor an assignment: it is not the writer's version to increment,
95
+ * nor its timestamps to set.
96
+ */
97
+ converge?(record: Record): Promise<boolean>;
98
+ /**
99
+ * Whether this storage converges. Absent is what a storage that does not says, and a
100
+ * component of a context that converges stands down rather than running where it would
101
+ * never take a record from another region.
102
+ */
103
+ readonly converges?: boolean;
84
104
  }
85
105
  /**
86
106
  * The subset of a component's entity declaration a storage reads. Structural, so that core
package/CHANGELOG.md DELETED
@@ -1,43 +0,0 @@
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.292](https://github.com/toa-io/toa/compare/v1.0.0-alpha.291...v1.0.0-alpha.292) (2026-09-07)
7
-
8
- **Note:** Version bump only for package @toa.io/core
9
-
10
-
11
-
12
-
13
-
14
- # [1.0.0-alpha.291](https://github.com/toa-io/toa/compare/v1.0.0-alpha.290...v1.0.0-alpha.291) (2026-09-07)
15
-
16
- **Note:** Version bump only for package @toa.io/core
17
-
18
-
19
-
20
-
21
-
22
- # [1.0.0-alpha.289](https://github.com/toa-io/toa/compare/v1.0.0-alpha.288...v1.0.0-alpha.289) (2026-09-07)
23
-
24
- * A deploy moves only what changed, and a component sees none of the runtime's environment (#1073) ([f38e3db](https://github.com/toa-io/toa/commit/f38e3db533f24db866c57bfa1ef294eff1eaf499)), closes [#1073](https://github.com/toa-io/toa/issues/1073) [#1064](https://github.com/toa-io/toa/issues/1064) [#1066](https://github.com/toa-io/toa/issues/1066) [#1067](https://github.com/toa-io/toa/issues/1067) [#1068](https://github.com/toa-io/toa/issues/1068) [#1069](https://github.com/toa-io/toa/issues/1069) [#1071](https://github.com/toa-io/toa/issues/1071) [#1070](https://github.com/toa-io/toa/issues/1070) [#1072](https://github.com/toa-io/toa/issues/1072)
25
-
26
- ### BREAKING CHANGES
27
-
28
- * a component that read `process.env.TOA_*` reads `context` instead;
29
- `echo(input)` no longer substitutes from the environment; a bash operation sees no
30
- `TOA_*`; images no longer set `USER node` — see migrations/289.md.
31
-
32
-
33
- # [1.0.0-alpha.288](https://github.com/toa-io/toa/compare/v1.0.0-alpha.287...v1.0.0-alpha.288) (2026-09-06)
34
-
35
- **Note:** Version bump only for package @toa.io/core
36
-
37
-
38
-
39
-
40
-
41
- # [1.0.0-alpha.287](https://github.com/toa-io/toa/compare/v1.0.0-alpha.286...v1.0.0-alpha.287) (2026-09-06)
42
-
43
- **Note:** Version bump only for package @toa.io/core
@@ -1,27 +0,0 @@
1
- import { Operation } from './operation.js'
2
- import type { Store } from './operation.js'
3
- import type { Changeset } from './entities/changeset.js'
4
-
5
- export class Assignment extends Operation {
6
- protected override async acquire(store: Store): Promise<void> {
7
- const changeset = this.scope.changeset(store.request.query as any)
8
-
9
- store.scope = changeset
10
- store.state = changeset.get()
11
- }
12
-
13
- protected override async commit(store: Store): Promise<void> {
14
- const { scope, state, reply, request } = store
15
-
16
- if (reply.error !== undefined) return
17
-
18
- const changeset = scope as Changeset
19
-
20
- changeset.set(state)
21
-
22
- const output = await this.scope.apply(changeset, request.input)
23
-
24
- // assignment returns new state by default
25
- if (store.reply.output === undefined) store.reply.output = output
26
- }
27
- }
package/source/call.ts DELETED
@@ -1,63 +0,0 @@
1
- import { Readable } from 'node:stream'
2
- import { current, encode } from 'openspan'
3
- import { Connector } from './connector.js'
4
- import type { Transmission } from './transmission.js'
5
- import type { Request as Contract } from './contract/request.js'
6
- import type { Request, Source } from './types/request.js'
7
-
8
- export class Call extends Connector {
9
- readonly #transmitter: Transmission
10
- readonly #contract: Contract
11
- readonly #source: Source | undefined
12
-
13
- public constructor(transmitter: Transmission, contract: Contract, source?: Source) {
14
- super()
15
-
16
- this.#transmitter = transmitter
17
- this.#contract = contract
18
- this.#source = source
19
-
20
- this.depends(transmitter)
21
- }
22
-
23
- public async invoke(request: Request = {}): Promise<any> {
24
- // the caller may have attributed the call itself, as the node bridge does
25
- if (this.#source !== undefined) request.source ??= this.#source
26
-
27
- this.#contract.fit(request)
28
-
29
- // an operation that takes no input still has to send an explicit null
30
- request.input ??= null
31
-
32
- // avoid validation on the recipient's side
33
- request.authentic = true
34
-
35
- const context = current()
36
-
37
- if (context !== undefined) request.telemetry = encode(context)
38
-
39
- const reply = await this.#transmitter.request(request)
40
-
41
- if (reply === null) return null
42
- else if (reply instanceof Readable) return reply
43
- else {
44
- if (reply.exception !== undefined) throw reply.exception
45
-
46
- if (reply.error !== undefined) return new RemoteError(reply.error)
47
- else return reply.output
48
- }
49
- }
50
-
51
- public explain(): any {
52
- return this.#contract.discovery
53
- }
54
- }
55
-
56
- // the remote error as a value: every property it carries, and nothing else enumerable
57
- class RemoteError extends Error {
58
- public constructor(error: object) {
59
- super()
60
-
61
- Object.assign(this, error)
62
- }
63
- }
package/source/cascade.ts DELETED
@@ -1,32 +0,0 @@
1
- import { Connector } from './connector.js'
2
- import type { Algorithm } from './types/bridges.js'
3
-
4
- export class Cascade extends Connector {
5
- readonly #last: Algorithm
6
-
7
- public constructor(bridges: Algorithm[], preflight?: Connector) {
8
- super()
9
-
10
- // this.#bridges = bridges
11
- this.#last = bridges[bridges.length - 1]
12
-
13
- if (preflight === undefined) this.depends(bridges)
14
- else this.depends(bridges).depends(preflight)
15
- }
16
-
17
- public async run(...args: [any, any?]): Promise<any> {
18
- // const reply = {}
19
- //
20
- // for (const bridge of this.#bridges) {
21
- // const partial = await bridge.execute(...args)
22
- //
23
- // if (partial.error) return { error: partial.error }
24
- //
25
- // merge(reply, partial)
26
- // }
27
- //
28
- // return reply
29
-
30
- return this.#last.execute(...args)
31
- }
32
- }
@@ -1,83 +0,0 @@
1
- import assert from 'node:assert'
2
- import { console, current, decode, run, type SpanOptions } from 'openspan'
3
- import { Connector } from './connector.js'
4
- import type { Locator } from './locator.js'
5
- import type { Request } from './types/request.js'
6
-
7
- /** What a component holds one of per endpoint: an operation, or the call that stands for it. */
8
- export interface Invocable extends Connector {
9
- invoke: (request: Request) => Promise<any>
10
- }
11
-
12
- export class Component<O extends Invocable = Invocable> extends Connector {
13
- public readonly locator: Locator
14
-
15
- protected readonly operations: Record<string, O>
16
-
17
- protected kind: 'server' | 'client' = 'server'
18
-
19
- /** span options per endpoint */
20
- readonly #spans: Record<string, SpanOptions> = {}
21
-
22
- public constructor(locator: Locator, operations: Record<string, O>) {
23
- super()
24
-
25
- this.locator = locator
26
- this.operations = operations
27
-
28
- Object.values(operations).forEach((operation) => this.depends(operation))
29
- }
30
-
31
- public async invoke<T = any>(endpoint: string, request?: Request): Promise<T> {
32
- if (!(endpoint in this.operations))
33
- // `assert.fail`, not `assert.ok`: the message is built only when it is needed
34
- assert.fail(`Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
35
-
36
- // if the request carries no telemetry, the trace starts here
37
- const remote = request?.telemetry === undefined ? null : decode(request.telemetry)
38
- const task = async (): Promise<any> => this.#process(endpoint, request)
39
-
40
- if (remote === null) return task()
41
- else return run(remote, task)
42
- }
43
-
44
- async #process(endpoint: string, request?: Request): Promise<any> {
45
- return console.span(this.#span(endpoint), async () => {
46
- const reply = await this.operations[endpoint].invoke(request as Request)
47
-
48
- if (reply?.exception !== undefined) {
49
- const span = current()
50
-
51
- if (span !== undefined) span.status = 'error'
52
-
53
- console.error('Failed to execute operation', {
54
- endpoint: `${this.locator.id}.${endpoint}`,
55
- exception: reply.exception
56
- })
57
- }
58
-
59
- return reply
60
- })
61
- }
62
-
63
- /**
64
- * The span of an endpoint never changes, so it is built once. Not in the constructor:
65
- * `kind` is a field of the subclass, and those are assigned after this one is built.
66
- *
67
- */
68
- #span(endpoint: string): SpanOptions {
69
- let options = this.#spans[endpoint]
70
-
71
- if (options === undefined) {
72
- options = { name: `${this.locator.id}.${endpoint}`, kind: this.kind }
73
-
74
- // the server span is emitted by the component itself, while the client span
75
- // belongs to the calling service and inherits it from the context
76
- if (this.kind === 'server') options.service = this.locator.id
77
-
78
- this.#spans[endpoint] = options
79
- }
80
-
81
- return options
82
- }
83
- }
@@ -1,27 +0,0 @@
1
- import { console } from 'openspan'
2
- import { Connector } from './connector.js'
3
-
4
- export class Composition extends Connector {
5
- // eslint-disable-next-line max-params
6
- public constructor(
7
- expositions: Connector[],
8
- producers: Connector[],
9
- receivers: Connector[],
10
- tenants: Connector[]
11
- ) {
12
- super()
13
-
14
- if (expositions.length > 0) this.depends(expositions)
15
- if (producers.length > 0) this.depends(producers)
16
- if (receivers.length > 0) this.depends(receivers)
17
- if (tenants.length > 0) this.depends(tenants)
18
- }
19
-
20
- protected override async open(): Promise<void> {
21
- console.info('Composition complete')
22
- }
23
-
24
- protected override async dispose(): Promise<void> {
25
- console.info('Composition shutdown complete')
26
- }
27
- }