@toa.io/core 1.0.0-alpha.291 → 1.0.0-alpha.293

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 (90) hide show
  1. package/package.json +11 -5
  2. package/CHANGELOG.md +0 -35
  3. package/source/assignment.ts +0 -27
  4. package/source/call.ts +0 -63
  5. package/source/cascade.ts +0 -32
  6. package/source/component.ts +0 -83
  7. package/source/composition.ts +0 -27
  8. package/source/connector.ts +0 -217
  9. package/source/context.ts +0 -68
  10. package/source/contract/contract.ts +0 -23
  11. package/source/contract/index.ts +0 -2
  12. package/source/contract/reply.ts +0 -56
  13. package/source/contract/request.ts +0 -140
  14. package/source/contract/schemas.ts +0 -39
  15. package/source/discovery.ts +0 -53
  16. package/source/effect.ts +0 -14
  17. package/source/emission.ts +0 -21
  18. package/source/entities/changeset.ts +0 -37
  19. package/source/entities/entity.ts +0 -137
  20. package/source/entities/factory.ts +0 -52
  21. package/source/entities/index.ts +0 -4
  22. package/source/entities/newid.ts +0 -9
  23. package/source/entities/set.ts +0 -27
  24. package/source/event.ts +0 -57
  25. package/source/exceptions.ts +0 -133
  26. package/source/exposition.ts +0 -33
  27. package/source/guard.ts +0 -16
  28. package/source/index.ts +0 -36
  29. package/source/locator.ts +0 -37
  30. package/source/observation.ts +0 -16
  31. package/source/operation.ts +0 -139
  32. package/source/outbox/index.ts +0 -1
  33. package/source/outbox/outbox.ts +0 -341
  34. package/source/query/criteria.ts +0 -90
  35. package/source/query/options.ts +0 -35
  36. package/source/query.ts +0 -67
  37. package/source/receiver.ts +0 -120
  38. package/source/reflection.ts +0 -20
  39. package/source/remote.ts +0 -20
  40. package/source/state.ts +0 -179
  41. package/source/transition.ts +0 -79
  42. package/source/transmission.ts +0 -38
  43. package/source/types/atomicity.ts +0 -63
  44. package/source/types/bindings.ts +0 -59
  45. package/source/types/bridges.ts +0 -54
  46. package/source/types/extensions.ts +0 -89
  47. package/source/types/index.ts +0 -14
  48. package/source/types/message.ts +0 -5
  49. package/source/types/operations.ts +0 -31
  50. package/source/types/outbox.ts +0 -35
  51. package/source/types/receiver.ts +0 -10
  52. package/source/types/request.ts +0 -54
  53. package/source/types/state.ts +0 -9
  54. package/source/types/storages.ts +0 -134
  55. package/source/unmanaged.ts +0 -8
  56. package/test/call.fixtures.js +0 -25
  57. package/test/call.test.js +0 -79
  58. package/test/component.fixtures.js +0 -18
  59. package/test/component.test.js +0 -57
  60. package/test/connector.fixtures.js +0 -56
  61. package/test/connector.test.js +0 -266
  62. package/test/context.fixtures.js +0 -14
  63. package/test/context.test.js +0 -40
  64. package/test/contract/conditions.test.js +0 -32
  65. package/test/contract/contract.fixtures.js +0 -28
  66. package/test/contract/request.test.js +0 -196
  67. package/test/discovery.test.js +0 -71
  68. package/test/emission.fixtures.js +0 -13
  69. package/test/emission.test.js +0 -60
  70. package/test/entities/entity.fixtures.js +0 -24
  71. package/test/entities/entity.test.js +0 -137
  72. package/test/entities/factory.fixtures.js +0 -31
  73. package/test/entities/factory.test.js +0 -99
  74. package/test/entities/set.fixtures.js +0 -9
  75. package/test/entities/set.test.js +0 -13
  76. package/test/event.fixtures.js +0 -24
  77. package/test/event.test.js +0 -162
  78. package/test/locator.test.js +0 -120
  79. package/test/outbox.test.js +0 -202
  80. package/test/query.fixtures.js +0 -96
  81. package/test/query.test.js +0 -183
  82. package/test/receiver.fixtures.js +0 -20
  83. package/test/receiver.test.js +0 -144
  84. package/test/reflection.test.js +0 -31
  85. package/test/state.fixtures.js +0 -49
  86. package/test/state.test.js +0 -162
  87. package/test/transmission.fixtures.js +0 -13
  88. package/test/transmission.test.js +0 -77
  89. package/tsconfig.json +0 -10
  90. package/tsconfig.tsbuildinfo +0 -1
@@ -1,341 +0,0 @@
1
- import { console } from 'openspan'
2
- import { Connector } from '../connector.js'
3
- import { newid } from '../entities/newid.js'
4
- import { environment } from '@toa.io/generic'
5
- import type { Emission } from '../emission.js'
6
- import type { Atom } from '../types/atomicity.js'
7
- import type { Storage } from '../types/storages.js'
8
- import type { Row } from '../types/outbox.js'
9
- import type { Event } from '../types/state.js'
10
-
11
- export interface Options {
12
- interval?: number
13
- batch?: number
14
- gap?: number
15
- }
16
-
17
- /**
18
- * Owns the intent to publish. A row is built before the write so that the storage can commit
19
- * it in the same transaction as the entity; publication then happens off the operation's path,
20
- * and anything that fails to publish is recovered from the row.
21
- *
22
- * The mechanism is a safety net: in a healthy system the row is written, published within
23
- * milliseconds by the same process, and marked published on that process's next tick.
24
- *
25
- * A storage that cannot commit a row atomically has no outbox, and this degrades to the
26
- * inline emission it replaces.
27
- */
28
- export class Outbox extends Connector {
29
- readonly #emission: Emission
30
- readonly #storage: Storage | undefined
31
- readonly #atom: Atom
32
-
33
- readonly #gap: number
34
- readonly #interval: number
35
- readonly #batch: number
36
- readonly #defer: boolean
37
-
38
- /** ids this process has published, held until a cycle marks them */
39
- readonly #published = new Set<string>()
40
-
41
- /** in-flight publications, awaited (with a bound) on close */
42
- readonly #inflight = new Set<Promise<void>>()
43
-
44
- /** rows this replica is publishing right now, so a cycle does not pick them up again */
45
- readonly #publishing = new Set<string>()
46
-
47
- #timer: NodeJS.Timeout | undefined
48
- #off: (() => void) | undefined
49
- #pumping = false
50
- #closing = false
51
-
52
- // eslint-disable-next-line max-params
53
- public constructor(
54
- emission: Emission,
55
- storage: Storage | undefined,
56
- atom: Atom,
57
- options: Options = {}
58
- ) {
59
- super()
60
-
61
- this.#emission = emission
62
- this.#storage = storage
63
- this.#atom = atom
64
-
65
- this.#interval = number('TOA_OUTBOX_INTERVAL', options.interval, INTERVAL)
66
- this.#batch = number('TOA_OUTBOX_BATCH', options.batch, BATCH)
67
- this.#gap = options.gap ?? this.#interval * K
68
- this.#defer = environment.get('TOA_OUTBOX_DEFER') === '1'
69
-
70
- this.depends(emission)
71
- this.depends(atom)
72
-
73
- if (storage !== undefined) this.depends(storage)
74
- }
75
-
76
- /** whether the storage can commit a row atomically with the entity */
77
- public get durable(): boolean {
78
- return this.#storage?.outbox !== undefined
79
- }
80
-
81
- /**
82
- * An assignment's images are the write's own, so it hands over an event with neither, and
83
- * the storage fills them in.
84
- */
85
- public row(event: Partial<Event>): Row {
86
- return {
87
- id: newid(),
88
- lane: this.#lane(),
89
- published: false,
90
- pending: Date.now() + this.#gap,
91
- event: event as Event
92
- }
93
- }
94
-
95
- /**
96
- * Hands a committed row over. Awaited by the caller only on the legacy path — with an
97
- * outbox this returns at once and the broker leaves the operation's path.
98
- */
99
- public publish(row: Row): Promise<void> | void {
100
- // without a durable outbox this is the inline path, and the caller awaits the emission
101
- if (!this.durable) return this.#emission.emit(row.event)
102
-
103
- /*
104
- * A publication started while the pump is closing would outlive the emitters it needs,
105
- * and `comq` waits on a connection that is going rather than failing. The row is already
106
- * durable, so leaving it is exactly what it is for.
107
- */
108
- if (
109
- this.#closing ||
110
- this.#defer ||
111
- this.#inflight.size >= INFLIGHT ||
112
- this.#published.size >= PUBLISHED
113
- )
114
- return
115
-
116
- void this.#publish(row)
117
- }
118
-
119
- protected override async open(): Promise<void> {
120
- if (!this.durable) return
121
-
122
- if (this.#defer)
123
- console.warn(
124
- 'Outbox immediate publication is deferred; events are published by the pump only'
125
- )
126
-
127
- this.#timer = setInterval(() => {
128
- this.#tick()
129
- }, this.#interval)
130
- this.#timer.unref()
131
-
132
- /*
133
- * A lane changing hands is exactly when rows stranded in it become this replica's to
134
- * publish, and the cycle would not notice for up to an interval. Being told costs a cycle
135
- * that finds nothing in the usual case, where the claim arrives once and never changes.
136
- */
137
- this.#off = this.#atom.onassigned(() => {
138
- this.#tick()
139
- })
140
- }
141
-
142
- protected override async close(): Promise<void> {
143
- this.#closing = true
144
-
145
- this.#off?.()
146
-
147
- if (this.#timer !== undefined) clearInterval(this.#timer)
148
-
149
- await this.#drain()
150
- await this.#mark()
151
- }
152
-
153
- /**
154
- * Publishes one row and swallows the failure: the row stays unpublished and comes back on a
155
- * later cycle, which is the whole point of having written it.
156
- *
157
- * There is no timeout here on purpose. A publication is a confirmed write to a durable
158
- * exchange, and `comq` waits for the broker to come back rather than failing — abandoning
159
- * it would not stop it, it would only mean the row is published twice once it lands. What
160
- * bounds this instead is the in-flight cap and the drain on close.
161
- *
162
- */
163
- async #publish(row: Row): Promise<void> {
164
- this.#publishing.add(row.id)
165
-
166
- const publishing = this.#emission.emit(row.event)
167
-
168
- this.#inflight.add(publishing)
169
-
170
- try {
171
- await publishing
172
-
173
- this.#published.add(row.id)
174
- } catch (error) {
175
- console.warn('Event publication failed', { row: row.id, error })
176
- } finally {
177
- this.#inflight.delete(publishing)
178
- this.#publishing.delete(row.id)
179
- }
180
- }
181
-
182
- /**
183
- * `comq` retries a publish for as long as the broker is down rather than rejecting, so an
184
- * unbounded drain outlives any grace period.
185
- *
186
- */
187
- async #drain(): Promise<void> {
188
- if (this.#inflight.size === 0) return
189
-
190
- await Promise.race([Promise.allSettled([...this.#inflight]), delay(DRAIN)])
191
- }
192
-
193
- /**
194
- * Reads what is due, publishes it, and marks everything this process has sent — what it just
195
- * published and what the immediate path published since the last cycle. One cycle at a time.
196
- *
197
- */
198
- #tick(): void {
199
- if (this.#pumping) return
200
-
201
- this.#pumping = true
202
-
203
- void this.#pump().finally(() => (this.#pumping = false))
204
- }
205
-
206
- async #pump(): Promise<void> {
207
- let page: Row[]
208
- let after: string | undefined
209
-
210
- do {
211
- page = await this.#read(after)
212
-
213
- if (page.length === 0) break
214
-
215
- // so a page is never read twice
216
- after = page[page.length - 1]?.id
217
-
218
- /*
219
- * A row is unpublished in the database until a cycle marks it, so a page includes what
220
- * this replica is sending right now and what a failed marking left behind. Only this
221
- * process knows either.
222
- */
223
- const rows = page.filter(
224
- (row) => !this.#published.has(row.id) && !this.#publishing.has(row.id)
225
- )
226
-
227
- if (rows.length > 0) {
228
- console.info('Outbox recovering unpublished events', { count: rows.length })
229
-
230
- // every row is given its chance; what the broker refused stays unpublished and comes
231
- // back on a later cycle
232
- await Promise.allSettled(rows.map(async (row) => this.#publish(row)))
233
- }
234
-
235
- // a full page is a page that may have been cut short
236
- } while (page.length === this.#batch)
237
-
238
- await this.#mark()
239
- }
240
-
241
- /**
242
- * One page of what is due. In a healthy system the first one is empty, every cycle — a row is
243
- * due only if the process that wrote it failed to publish or died before marking it.
244
- *
245
- * Reading is suspended, not stopped, while this replica does not know which lanes are its
246
- * own: the cycle keeps running and keeps marking, and reading resumes as soon as an
247
- * assignment arrives. Reading without an assignment would be a different guarantee, where
248
- * every replica publishes every stranded row.
249
- *
250
- * @param after the last id of the page before, so a page is never read twice
251
- */
252
- async #read(after?: string): Promise<Row[]> {
253
- const lanes = this.#atom.slots(LANES)
254
-
255
- if (lanes === null || lanes.length === 0) return []
256
-
257
- return this.#storage!
258
- .outbox!.pending(lanes, Date.now(), this.#batch, after)
259
- .catch((error) => {
260
- console.warn('Outbox read failed', { error })
261
-
262
- return []
263
- })
264
- }
265
-
266
- /**
267
- * One batched write for many events, which is why the ids are held in memory rather than
268
- * marked one by one. Ids that fail to be marked are kept and retried; a row that is never
269
- * marked is simply published again, which is within the contract.
270
- *
271
- */
272
- async #mark(): Promise<void> {
273
- if (this.#published.size === 0) return
274
-
275
- const ids = [...this.#published]
276
-
277
- try {
278
- await this.#storage!.outbox!.settle(ids)
279
-
280
- for (const id of ids) this.#published.delete(id)
281
- } catch (error) {
282
- console.warn('Outbox marking failed', { count: ids.length, error })
283
- }
284
- }
285
-
286
- /**
287
- * A lane this replica currently owns, so that in steady state it settles its own rows
288
- * before it ever reads them. Any lane at all when it owns none: the row still has to be
289
- * written, and whoever ends up owning that lane will pump it.
290
- *
291
- */
292
- #lane(): number {
293
- const owned = this.#atom.slots(LANES)
294
-
295
- return owned === null || owned.length === 0
296
- ? Math.floor(Math.random() * LANES)
297
- : owned[Math.floor(Math.random() * owned.length)]
298
- }
299
- }
300
-
301
- function number(
302
- variable: string,
303
- declared: number | undefined,
304
- fallback: number
305
- ): number {
306
- if (declared !== undefined) return declared
307
-
308
- const value = Number(environment.get(variable))
309
-
310
- return Number.isNaN(value) || value <= 0 ? fallback : value
311
- }
312
-
313
- async function delay(ms: number): Promise<void> {
314
- return new Promise((resolve) => {
315
- setTimeout(resolve, ms).unref()
316
- })
317
- }
318
-
319
- /**
320
- * Constant, never configuration: rows carry their lane, so lowering this would leave rows in
321
- * lanes nobody reads any more. It is also the ceiling on replicas of one component, and a
322
- * power of two so that the common replica counts divide evenly.
323
- */
324
- export const LANES = 128
325
-
326
- /** one cycle reads, publishes and marks; in steady state it finds nothing to read */
327
- const INTERVAL = 5000
328
-
329
- /**
330
- * `gap = interval * K`. Not a steady-state necessity — a replica writes into a lane it owns
331
- * and marks what it published — but a guard for when a lane changes hands between the write
332
- * and the settle. Two cycles of separation, plus one of margin.
333
- */
334
- const K = 3
335
-
336
- /** how many rows one read brings back; the pump reads on while a page comes back full */
337
- const BATCH = 200
338
-
339
- const DRAIN = 10_000
340
- const INFLIGHT = 1000
341
- const PUBLISHED = 10_000
@@ -1,90 +0,0 @@
1
- import { parse } from '@rsql/parser'
2
- import { QuerySyntaxException } from '../exceptions.js'
3
- import type { Node } from '../types/storages.js'
4
-
5
- /** What a component declares about the properties a criteria may select on. */
6
- export type Properties = Record<string, { type: string }>
7
-
8
- export function criteria(expression: string, properties?: Properties): Node {
9
- let ast: Node
10
-
11
- try {
12
- ast = parse(expression) as unknown as Node
13
- } catch (e) {
14
- throw new QuerySyntaxException((e as Error).message)
15
- }
16
-
17
- if (properties !== undefined) read(ast, properties)
18
-
19
- return ast
20
- }
21
-
22
- function read(node: Node, properties: Properties): void {
23
- if (
24
- node.type === 'COMPARISON' &&
25
- node.left?.type === 'SELECTOR' &&
26
- node.right?.type === 'VALUE'
27
- ) {
28
- const selector = node.left.selector as string
29
- const property = properties[selector]
30
-
31
- if (property === undefined) {
32
- throw new QuerySyntaxException(`Criteria selector '${selector}' is not defined`)
33
- }
34
-
35
- const cast = CAST[property.type]
36
-
37
- // `=in=` and `=out=` carry a list, and casting that as one value gives whatever
38
- // a comma-separated string reads as
39
- if (cast !== undefined)
40
- node.right.value = Array.isArray(node.right.value)
41
- ? node.right.value.map((value: string) => cast(value, selector))
42
- : cast(node.right.value as string, selector)
43
- } else {
44
- if (node.left !== undefined) read(node.left, properties)
45
- if (node.right !== undefined) read(node.right, properties)
46
- }
47
- }
48
-
49
- /**
50
- * A criteria arrives as text, so a value is read as what the property it selects on holds. What
51
- * cannot be read as that is refused rather than passed on: `parseInt` answers `NaN` for a word
52
- * and `12` for `12kg`, and a storage asked to match either finds nothing and says nothing —
53
- * a mistyped filter reads as an empty result.
54
- *
55
- * A string is left as it came, because every text is one: what a criteria may compare a string
56
- * to is not the type's to say.
57
- */
58
- const CAST: Record<string, (value: string, selector: string) => unknown> = {
59
- number: (value, selector) => finite(value, selector, 'a number'),
60
-
61
- integer: (value, selector) => {
62
- const number = finite(value, selector, 'an integer')
63
-
64
- if (!Number.isInteger(number)) refuse(selector, 'an integer', value)
65
-
66
- return number
67
- },
68
-
69
- boolean: (value, selector) => {
70
- if (value === 'true') return true
71
- if (value === 'false') return false
72
-
73
- return refuse(selector, 'a boolean', value)
74
- }
75
- }
76
-
77
- function finite(value: string, selector: string, expected: string): number {
78
- // `Number` reads a blank string as zero, and a criteria that says nothing says nothing
79
- const number = value.trim() === '' ? Number.NaN : Number(value)
80
-
81
- if (!Number.isFinite(number)) refuse(selector, expected, value)
82
-
83
- return number
84
- }
85
-
86
- function refuse(selector: string, expected: string, value: string): never {
87
- throw new QuerySyntaxException(
88
- `Criteria selector '${selector}' takes ${expected}, and '${value}' is not one`
89
- )
90
- }
@@ -1,35 +0,0 @@
1
- import { QuerySyntaxException } from '../exceptions.js'
2
- import type { Options } from '../types/storages.js'
3
- import type { Properties } from './criteria.js'
4
-
5
- export function options(given: Record<string, any>, properties: Properties): Options {
6
- if (given.sort !== undefined) given.sort = sort(given.sort, properties)
7
-
8
- if (given.projection !== undefined) projection(given.projection, properties)
9
-
10
- return given as Options
11
- }
12
-
13
- function sort(sort: string[], properties: Properties): Array<[string, string]> {
14
- const result: Array<[string, string]> = []
15
-
16
- for (const sorting of sort) {
17
- const [property, direction] = sorting.split(':')
18
-
19
- if (properties[property] === undefined)
20
- throw new QuerySyntaxException(`Sort property '${property}' is not defined`)
21
-
22
- result.push([property, direction ?? 'asc'])
23
- }
24
-
25
- return result
26
- }
27
-
28
- function projection(projection: string[], properties: Properties): void {
29
- for (const property of projection)
30
- if (properties[property] === undefined)
31
- throw new QuerySyntaxException(`Projection property '${property}' is not defined`)
32
-
33
- for (const property of ['VERSION', 'CREATED', 'UPDATED', 'DELETED'])
34
- if (!projection.includes(property)) projection.push(property)
35
- }
package/source/query.ts DELETED
@@ -1,67 +0,0 @@
1
- import { empty } from '@toa.io/generic'
2
- import * as criteria from './query/criteria.js'
3
- import * as options from './query/options.js'
4
- import type { Properties } from './query/criteria.js'
5
- import type { Node, Options, Query as Parsed } from './types/storages.js'
6
- import type { Query as Requested } from './types/request.js'
7
-
8
- const parse = { ...criteria, ...options }
9
-
10
- /** Translates the query a request carries into the one a storage is given. */
11
- export class Query {
12
- readonly #properties: Properties
13
-
14
- /** parsed criteria by their expression */
15
- readonly #asts = new Map<string, Node>()
16
-
17
- public constructor(properties: Properties) {
18
- this.#properties = properties
19
- }
20
-
21
- public parse(query: Requested): Parsed {
22
- const result: Parsed = {}
23
- const { id, ids, version, criteria, search, ...rest } = query
24
-
25
- const options = this.#options(rest)
26
-
27
- if (id !== undefined) result.id = id
28
- if (ids !== undefined) result.ids = ids
29
- if (version !== undefined) result.version = version
30
- if (criteria !== undefined) result.criteria = this.#criteria(criteria)
31
- if (search !== undefined) result.search = search
32
- if (options !== undefined) result.options = options
33
-
34
- return result
35
- }
36
-
37
- #options(given: Record<string, any>): Options | undefined {
38
- if (empty(given)) return undefined
39
-
40
- return parse.options(given, this.#properties)
41
- }
42
-
43
- /**
44
- * Lexing an RSQL expression is the most expensive thing this class does, and the same
45
- * expression arrives over and over: a route builds it from its own declaration and the
46
- * request parameters. The tree is only read downstream — a storage builds a fresh query
47
- * from it — so it is kept rather than parsed again.
48
- *
49
- * Expressions come from the client, hence the bound. An invalid one throws before it
50
- * reaches the cache.
51
- */
52
- #criteria(criteria: string): Node {
53
- const known = this.#asts.get(criteria)
54
-
55
- if (known !== undefined) return known
56
-
57
- const ast = parse.criteria(criteria, this.#properties)
58
-
59
- if (this.#asts.size >= LIMIT) this.#asts.clear()
60
-
61
- this.#asts.set(criteria, ast)
62
-
63
- return ast
64
- }
65
- }
66
-
67
- const LIMIT = 1024
@@ -1,120 +0,0 @@
1
- import { console, decode, run, type SpanOptions } from 'openspan'
2
- import { add } from '@toa.io/generic'
3
- import { Connector } from './connector.js'
4
- import type { Component } from './component.js'
5
- import type { Receiver as Bridge } from './types/bridges.js'
6
- import type { Message } from './types/message.js'
7
- import type { Request, Source } from './types/request.js'
8
-
9
- export interface Definition {
10
- conditioned?: boolean
11
- adaptive?: boolean
12
- operation: string
13
- label?: string
14
- destination?: string
15
- arguments?: unknown[]
16
- origin?: Source
17
- }
18
-
19
- export class Receiver extends Connector {
20
- readonly #conditioned: boolean | undefined
21
- readonly #adaptive: boolean | undefined
22
- readonly #endpoint: string
23
- readonly #label: string
24
- readonly #destination: string
25
- readonly #arguments: unknown[] | undefined
26
- readonly #origin: Source | undefined
27
- readonly #local: Component
28
- readonly #bridge: Bridge | undefined
29
- readonly #delivery: SpanOptions
30
- readonly #processing: SpanOptions
31
-
32
- public constructor(definition: Definition, local: Component, bridge?: Bridge) {
33
- super()
34
-
35
- const { conditioned, adaptive, operation } = definition
36
-
37
- this.#conditioned = conditioned
38
- this.#adaptive = adaptive
39
- this.#endpoint = operation
40
- this.#label = definition.label ?? operation
41
- this.#destination = definition.destination ?? this.#label
42
- this.#arguments = definition.arguments
43
- this.#origin = definition.origin
44
-
45
- this.#local = local
46
- this.#bridge = bridge
47
-
48
- this.depends(local)
49
- if (bridge !== undefined) this.depends(bridge)
50
-
51
- /*
52
- * The delivery span is created on behalf of the messaging destination, so that
53
- * each consumer forms its own complete producer/consumer pair, and service graphs
54
- * display fan-out correctly: producer -> destination -> each consumer
55
- * (Tempo pairs spans one-to-one, thus multiple consumers can not pair
56
- * with a single producer span, see grafana/tempo#5408)
57
- */
58
- this.#delivery = {
59
- name: `${this.#label} deliver`,
60
- kind: 'producer',
61
- service: this.#destination,
62
- attributes: { 'messaging.destination.name': this.#destination }
63
- }
64
-
65
- this.#processing = {
66
- name: `${this.#label} process`,
67
- kind: 'consumer',
68
- service: local.locator.id,
69
- attributes: { 'messaging.destination.name': this.#destination }
70
- }
71
- }
72
-
73
- /** @hot */
74
- public async receive(message: Message): Promise<void> {
75
- const { payload, telemetry, ...extensions } = message
76
-
77
- if (this.#conditioned === true && (await this.#bridge?.condition(payload)) === false)
78
- return
79
-
80
- const request = await this.#request(payload)
81
-
82
- add(request, extensions)
83
-
84
- // set after `add`, so that a message field can not spoof the origin
85
- if (this.#origin !== undefined) request.source = this.#origin
86
-
87
- // continue the trace from the producer span
88
- const remote = telemetry === undefined ? null : decode(telemetry)
89
- const task = async (): Promise<void> => {
90
- await this.#process(request)
91
- }
92
-
93
- if (remote === null) await task()
94
- else await run(remote, task)
95
- }
96
-
97
- async #process(request: Request): Promise<void> {
98
- return console.span(this.#delivery, async () =>
99
- console.span(this.#processing, async () => {
100
- try {
101
- await this.#local.invoke(this.#endpoint, request)
102
- } catch (error) {
103
- console.error('Receiver error', {
104
- component: this.#local.locator.id,
105
- endpoint: this.#endpoint,
106
- error
107
- })
108
-
109
- throw error
110
- }
111
- })
112
- )
113
- }
114
-
115
- async #request(payload: object): Promise<Request> {
116
- return this.#adaptive === true
117
- ? await this.#bridge!.request(payload, ...(this.#arguments ?? []))
118
- : { input: payload }
119
- }
120
- }
@@ -1,20 +0,0 @@
1
- import { Connector } from './connector.js'
2
-
3
- /** What a reflection reads, once, as it connects. */
4
- export type Source<T = any> = () => Promise<T>
5
-
6
- export class Reflection<T = any> extends Connector {
7
- public value: T | undefined
8
-
9
- readonly #source: Source<T>
10
-
11
- public constructor(source: Source<T>) {
12
- super()
13
-
14
- this.#source = source
15
- }
16
-
17
- protected override async open(): Promise<void> {
18
- this.value = await this.#source()
19
- }
20
- }
package/source/remote.ts DELETED
@@ -1,20 +0,0 @@
1
- import assert from 'node:assert'
2
- import { Component } from './component.js'
3
- import type { Invocable } from './component.js'
4
-
5
- /** A call knows what its endpoint declares, which is what an explanation is read from. */
6
- export interface Explicable extends Invocable {
7
- explain(): any
8
- }
9
-
10
- export class Remote extends Component<Explicable> {
11
- protected override kind = 'client' as const
12
-
13
- public explain(endpoint: string): any {
14
- if (!(endpoint in this.operations))
15
- // `assert.fail`, not `assert.ok`: the message is built only when it is needed
16
- assert.fail(`Endpoint '${endpoint}' is not provided by '${this.locator.id}'`)
17
-
18
- return this.operations[endpoint].explain()
19
- }
20
- }