@toa.io/extensions.realtime 1.0.0-alpha.28 → 1.0.0-alpha.283

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 (85) hide show
  1. package/CHANGELOG.md +337 -0
  2. package/components/realtime.streams/manifest.toa.yaml +47 -0
  3. package/components/{streams → realtime.streams}/operations/create.d.ts +7 -4
  4. package/components/realtime.streams/operations/create.js +65 -0
  5. package/components/realtime.streams/operations/create.js.map +1 -0
  6. package/components/realtime.streams/operations/lib/Stash.d.ts +21 -0
  7. package/components/realtime.streams/operations/lib/Stash.js +86 -0
  8. package/components/realtime.streams/operations/lib/Stash.js.map +1 -0
  9. package/components/realtime.streams/operations/lib/Stream.d.ts +13 -0
  10. package/components/realtime.streams/operations/lib/Stream.js +43 -0
  11. package/components/realtime.streams/operations/lib/Stream.js.map +1 -0
  12. package/components/realtime.streams/operations/lib/index.d.ts +2 -0
  13. package/components/realtime.streams/operations/lib/index.js +3 -0
  14. package/components/realtime.streams/operations/lib/index.js.map +1 -0
  15. package/components/{streams → realtime.streams}/operations/push.d.ts +1 -1
  16. package/components/realtime.streams/operations/push.js +10 -0
  17. package/components/realtime.streams/operations/push.js.map +1 -0
  18. package/components/realtime.streams/source/create.ts +97 -0
  19. package/components/realtime.streams/source/lib/Stash.ts +133 -0
  20. package/components/realtime.streams/source/lib/Stream.ts +56 -0
  21. package/components/realtime.streams/source/lib/index.ts +2 -0
  22. package/components/realtime.streams/source/push.ts +12 -0
  23. package/components/{streams → realtime.streams}/tsconfig.json +1 -0
  24. package/components/realtime.streams/tsconfig.tsbuildinfo +1 -0
  25. package/components/realtime.streams/types/index.d.ts +20 -0
  26. package/components/realtime.streams/types/toa.d.ts +29 -0
  27. package/{components/context.toa.yaml → context.toa.yaml} +0 -1
  28. package/cucumber.mjs +5 -0
  29. package/features/static.feature +120 -0
  30. package/features/steps/Components.ts +14 -14
  31. package/features/steps/Realtime.ts +14 -18
  32. package/features/steps/Streams.ts +51 -13
  33. package/features/steps/components/messages/manifest.toa.yaml +37 -6
  34. package/features/steps/config.ts +12 -0
  35. package/package.json +8 -7
  36. package/readme.md +33 -7
  37. package/source/Composition.ts +24 -13
  38. package/source/Factory.ts +11 -13
  39. package/source/Realtime.ts +41 -13
  40. package/source/Receiver.ts +86 -0
  41. package/source/Routes.ts +14 -37
  42. package/source/extension.ts +82 -0
  43. package/source/index.ts +2 -1
  44. package/transpiled/Composition.d.ts +10 -4
  45. package/transpiled/Composition.js +21 -20
  46. package/transpiled/Composition.js.map +1 -1
  47. package/transpiled/Factory.d.ts +4 -4
  48. package/transpiled/Factory.js +13 -18
  49. package/transpiled/Factory.js.map +1 -1
  50. package/transpiled/Realtime.d.ts +3 -2
  51. package/transpiled/Realtime.js +36 -11
  52. package/transpiled/Realtime.js.map +1 -1
  53. package/transpiled/Receiver.d.ts +25 -0
  54. package/transpiled/Receiver.js +55 -0
  55. package/transpiled/Receiver.js.map +1 -0
  56. package/transpiled/Routes.d.ts +5 -10
  57. package/transpiled/Routes.js +14 -31
  58. package/transpiled/Routes.js.map +1 -1
  59. package/transpiled/extension.d.ts +19 -0
  60. package/transpiled/extension.js +48 -0
  61. package/transpiled/extension.js.map +1 -0
  62. package/transpiled/index.d.ts +2 -1
  63. package/transpiled/index.js +2 -5
  64. package/transpiled/index.js.map +1 -1
  65. package/tsconfig.json +2 -1
  66. package/tsconfig.tsbuildinfo +1 -0
  67. package/components/streams/manifest.toa.yaml +0 -21
  68. package/components/streams/operations/create.js +0 -29
  69. package/components/streams/operations/create.js.map +0 -1
  70. package/components/streams/operations/lib/stream.d.ts +0 -10
  71. package/components/streams/operations/lib/stream.js +0 -31
  72. package/components/streams/operations/lib/stream.js.map +0 -1
  73. package/components/streams/operations/push.js +0 -8
  74. package/components/streams/operations/push.js.map +0 -1
  75. package/components/streams/operations/tsconfig.tsbuildinfo +0 -1
  76. package/components/streams/operations/types.d.ts +0 -11
  77. package/components/streams/operations/types.js +0 -3
  78. package/components/streams/operations/types.js.map +0 -1
  79. package/components/streams/source/create.ts +0 -38
  80. package/components/streams/source/lib/stream.ts +0 -37
  81. package/components/streams/source/push.ts +0 -5
  82. package/components/streams/source/types.ts +0 -13
  83. package/cucumber.js +0 -8
  84. package/stage/streams.test.ts +0 -128
  85. package/transpiled/tsconfig.tsbuildinfo +0 -1
@@ -1,11 +1,13 @@
1
+ import { console, run } from 'openspan'
1
2
  import { type Component, Connector } from '@toa.io/core'
2
- import { type Routes } from './Routes'
3
+ import { type Routes } from './Routes.js'
4
+ import type { Push } from './Receiver.js'
3
5
 
4
6
  export class Realtime extends Connector {
5
- private readonly discovery: Promise<Component>
7
+ private readonly discovery: () => Promise<Component>
6
8
  private streams: Component | null = null
7
9
 
8
- public constructor (routes: Routes, discovery: Promise<Component>) {
10
+ public constructor (routes: Routes, discovery: () => Promise<Component>) {
9
11
  super()
10
12
 
11
13
  this.discovery = discovery
@@ -14,25 +16,51 @@ export class Realtime extends Connector {
14
16
  }
15
17
 
16
18
  protected override async open (): Promise<void> {
17
- this.streams = await this.discovery
19
+ // the lookup belongs here, not in the constructor: dependencies connect before
20
+ // open, so the composition that serves realtime.streams is up to answer it. Asked
21
+ // any earlier the lookup goes unanswered, and it waits without a bound.
22
+ this.streams = await this.discovery()
18
23
  this.depends(this.streams)
19
24
 
20
25
  await this.streams.connect()
21
26
 
22
- console.log('Realtime has started.')
27
+ console.info('Realtime service started')
23
28
  }
24
29
 
25
30
  protected override dispose (): void {
26
- console.log('Realtime shutdown complete.')
31
+ console.info('Realtime service shutdown complete')
27
32
  }
28
33
 
29
- private push (event: Event): void {
30
- void this.streams?.invoke('push', { input: event })
34
+ private push ({ telemetry, ...event }: Push): void {
35
+ const processing = telemetry === null
36
+ ? this.deliver(event)
37
+ : run(telemetry, async () => await this.deliver(event))
38
+
39
+ void processing.catch((error) => console.error('Realtime push failed', error))
31
40
  }
32
- }
33
41
 
34
- interface Event {
35
- key: string
36
- event: string
37
- data: string
42
+ private async deliver (event: Omit<Push, 'telemetry'>): Promise<void> {
43
+ /*
44
+ * The delivery span is created on behalf of the messaging destination
45
+ * (same as the core Receiver), so that service graphs display the fan-out:
46
+ * producer -> destination -> realtime
47
+ */
48
+ const delivery = {
49
+ name: `${event.event} deliver`,
50
+ kind: 'producer' as const,
51
+ service: event.event,
52
+ attributes: { 'messaging.destination.name': event.event }
53
+ }
54
+
55
+ const options = {
56
+ name: `${event.event} push`,
57
+ kind: 'consumer' as const,
58
+ service: 'realtime',
59
+ attributes: { 'messaging.destination.name': event.event }
60
+ }
61
+
62
+ await console.span(delivery, async () => await console.span(options, async () => {
63
+ await this.streams?.invoke('push', { input: event })
64
+ }))
65
+ }
38
66
  }
@@ -0,0 +1,86 @@
1
+ import { console, decode, run } from 'openspan'
2
+ import { Connector, type Message } from '@toa.io/core'
3
+ import type { SpanContext } from 'openspan'
4
+ import type { Readable } from 'node:stream'
5
+
6
+ export class Receiver extends Connector {
7
+ private readonly event: string
8
+ private readonly properties: string[]
9
+ private readonly expose?: string[]
10
+ private readonly stream: Readable
11
+
12
+ public constructor ({ event, properties, stream, expose }: {
13
+ event: string
14
+ properties: string[]
15
+ stream: Readable
16
+ expose?: string[]
17
+ }) {
18
+ super()
19
+
20
+ this.event = event
21
+ this.properties = properties
22
+ this.expose = expose
23
+ this.stream = stream
24
+ }
25
+
26
+ public async receive (message: Message<Record<string, string>>): Promise<void> {
27
+ // the push continues the trace from the producer
28
+ const telemetry = message.telemetry === undefined ? null : decode(message.telemetry)
29
+
30
+ if (telemetry === null)
31
+ this.process(message, telemetry)
32
+ else
33
+ run(telemetry, () => this.process(message, telemetry))
34
+ }
35
+
36
+ private process (message: Message<Record<string, string>>, telemetry: SpanContext | null): void {
37
+ const data = this.fit(message.payload)
38
+
39
+ for (const property of this.properties) {
40
+ const key = message.payload[property]
41
+
42
+ if (key === undefined) {
43
+ console.debug('Event does not contain key property',
44
+ { property, event: this.event })
45
+
46
+ continue
47
+ }
48
+
49
+ if (Array.isArray(key))
50
+ // eslint-disable-next-line max-depth
51
+ for (const k of key as string[])
52
+ this.push(k, data, telemetry)
53
+ else
54
+ this.push(key, data, telemetry)
55
+ }
56
+ }
57
+
58
+ private fit (payload: Record<string, string>): Record<string, string> {
59
+ if (this.expose === undefined)
60
+ return payload
61
+
62
+ const entries = Object.entries(payload)
63
+ .filter(([key]) => this.expose!.includes(key))
64
+
65
+ return Object.fromEntries(entries)
66
+ }
67
+
68
+ private push (key: string | null, data: Record<string, string>, telemetry: SpanContext | null): void {
69
+ if (key === null || typeof key === 'undefined') {
70
+ console.debug('Key is null or undefined, skipping', { key, event: this.event })
71
+
72
+ return
73
+ }
74
+
75
+ console.debug('Pushing event to stream', { key, event: this.event, data })
76
+
77
+ this.stream.push({ key, event: this.event, data, telemetry } satisfies Push)
78
+ }
79
+ }
80
+
81
+ export interface Push {
82
+ key: string
83
+ event: string
84
+ data: Record<string, string>
85
+ telemetry: SpanContext | null
86
+ }
package/source/Routes.ts CHANGED
@@ -1,32 +1,34 @@
1
1
  import { Readable } from 'node:stream'
2
- import { Connector, type Message } from '@toa.io/core'
3
- import { decode } from '@toa.io/generic'
4
- import { type Bootloader } from './Factory'
2
+ import { console } from 'openspan'
3
+ import { Connector } from '@toa.io/core'
4
+ import { Receiver } from './Receiver.js'
5
+ import type { Route } from './extension.js'
6
+ import type { Host } from './Factory.js'
5
7
 
6
8
  export class Routes extends Connector {
7
9
  public events = new Events()
8
10
 
9
- private readonly boot: Bootloader
11
+ private readonly host: Host
10
12
 
11
- public constructor (boot: Bootloader) {
13
+ public constructor (host: Host) {
12
14
  super()
13
15
 
14
- this.boot = boot
16
+ this.host = host
15
17
  }
16
18
 
17
19
  private static read (): Route[] {
18
20
  if (process.env.TOA_REALTIME === undefined)
19
21
  throw new Error('TOA_REALTIME is not defined')
20
22
 
21
- return decode<Route[]>(process.env.TOA_REALTIME)
23
+ return JSON.parse(process.env.TOA_REALTIME) as Route[]
22
24
  }
23
25
 
24
26
  public override async open (): Promise<void> {
25
27
  const routes = Routes.read()
26
28
  const creating = []
27
29
 
28
- for (const { event, properties } of routes) {
29
- const consumer = this.boot.receive(event, this.getReceiver(event, properties))
30
+ for (const { event, properties, expose } of routes) {
31
+ const consumer = this.host.receive(event, new Receiver({ event, properties, stream: this.events, expose }))
30
32
 
31
33
  creating.push(consumer)
32
34
  }
@@ -39,29 +41,11 @@ export class Routes extends Connector {
39
41
  await Promise.all(connecting)
40
42
  this.depends(consumers)
41
43
 
42
- console.log(`Event sources (${creating.length}) connected.`)
44
+ console.info('Event sources connected', { count: creating.length })
43
45
  }
44
46
 
45
47
  public override async close (): Promise<void> {
46
- console.log('Event sources disconnected.')
47
- }
48
-
49
- private getReceiver (event: string, properties: string[]): Receiver {
50
- return {
51
- receive: (message: Message<Record<string, string>>) => {
52
- for (const property of properties) {
53
- const key = message.payload[property]
54
-
55
- if (key === undefined) {
56
- console.error(`Event '${event}' does not contain the '${property}' property.`)
57
-
58
- return
59
- }
60
-
61
- this.events.push({ key, event, data: message.payload })
62
- }
63
- }
64
- }
48
+ console.info('Event sources disconnected')
65
49
  }
66
50
  }
67
51
 
@@ -74,11 +58,4 @@ class Events extends Readable {
74
58
  }
75
59
  }
76
60
 
77
- export interface Route {
78
- event: string
79
- properties: string[]
80
- }
81
-
82
- interface Receiver {
83
- receive: (message: Message<Record<string, string>>) => void
84
- }
61
+ export type { Route }
@@ -0,0 +1,82 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { components } from './Composition.js'
3
+ import type { Dependency, Instances, Resources, Service } from '@toa.io/operations'
4
+
5
+ export const standalone = true
6
+ export { components } from './Composition.js'
7
+
8
+ export function deployment (instances: Instances<Declaration>, annotation?: Declaration & Annotation): Dependency {
9
+ const routes = []
10
+ const { resources, ...annotatedRoutes } = annotation ?? {}
11
+ const labels = components().labels
12
+
13
+ if (annotatedRoutes !== undefined)
14
+ routes.push(...parse(annotatedRoutes))
15
+
16
+ for (const instance of instances) {
17
+ const completed: Declaration = {}
18
+
19
+ for (const [key, value] of Object.entries(instance.manifest)) {
20
+ const event = instance.locator.id + '.' + key
21
+
22
+ completed[event] = value
23
+ }
24
+
25
+ routes.push(...parse(completed))
26
+ }
27
+
28
+ const service: Service = {
29
+ group: 'realtime',
30
+ name: 'streams',
31
+
32
+ version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version,
33
+ components: labels,
34
+ resources,
35
+ variables: [{
36
+ name: 'TOA_REALTIME',
37
+ value: JSON.stringify(routes)
38
+ }]
39
+ }
40
+
41
+ return { services: [service], events: routes.map((route) => route.event) }
42
+ }
43
+
44
+ export function parse (declaration: Declaration): Route[] {
45
+ const routes: Route[] = []
46
+
47
+ for (const [event, value] of Object.entries(declaration))
48
+ if (isObject(value)) {
49
+ const properties = Array.isArray(value.key) ? value.key : [value.key]
50
+
51
+ routes.push({ event, properties, expose: value.expose })
52
+ } else {
53
+ const properties = Array.isArray(value) ? value : [value]
54
+
55
+ routes.push({ event, properties })
56
+ }
57
+
58
+ return routes
59
+ }
60
+
61
+ function isObject (value: Entry): value is RouteDeclaration {
62
+ return typeof value === 'object' && !Array.isArray(value)
63
+ }
64
+
65
+ export type Entry = string | string[] | RouteDeclaration
66
+
67
+ export type Declaration = Record<string, Entry>
68
+
69
+ export interface RouteDeclaration {
70
+ key: string | string[]
71
+ expose?: string[]
72
+ }
73
+
74
+ export interface Route {
75
+ event: string
76
+ properties: string[]
77
+ expose?: string[]
78
+ }
79
+
80
+ interface Annotation {
81
+ resources?: Resources
82
+ }
package/source/index.ts CHANGED
@@ -1 +1,2 @@
1
- export { Factory } from './Factory'
1
+ export { Factory } from './Factory.js'
2
+ export * from './extension.js'
@@ -1,8 +1,14 @@
1
1
  import { Connector } from '@toa.io/core';
2
- import { type Bootloader } from './Factory';
2
+ import { type Host } from './Factory.js';
3
3
  export declare class Composition extends Connector {
4
- private readonly boot;
5
- constructor(boot: Bootloader);
4
+ private readonly host;
5
+ constructor(host: Host);
6
6
  protected open(): Promise<void>;
7
- protected dispose(): void;
8
7
  }
8
+ export declare function find(): string[];
9
+ export declare function components(): Components;
10
+ interface Components {
11
+ labels: string[];
12
+ paths: string[];
13
+ }
14
+ export {};
@@ -1,33 +1,34 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Composition = void 0;
4
- const node_fs_1 = require("node:fs");
5
- const node_path_1 = require("node:path");
6
- const core_1 = require("@toa.io/core");
7
- class Composition extends core_1.Connector {
8
- boot;
9
- constructor(boot) {
1
+ import { readdirSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { Connector } from '@toa.io/core';
4
+ export class Composition extends Connector {
5
+ host;
6
+ constructor(host) {
10
7
  super();
11
- this.boot = boot;
8
+ this.host = host;
12
9
  }
13
10
  async open() {
14
11
  const paths = find();
15
- const composition = await this.boot.composition(paths);
12
+ const composition = await this.host.composition(paths);
16
13
  await composition.connect();
17
14
  this.depends(composition);
18
- console.info('Composition complete.');
19
- }
20
- dispose() {
21
- console.info('Composition shutdown complete.');
22
15
  }
23
16
  }
24
- exports.Composition = Composition;
25
- function find() {
26
- return entries().map((entry) => (0, node_path_1.resolve)(ROOT, entry.name));
17
+ export function find() {
18
+ return entries().map((entry) => resolve(ROOT, entry.name));
27
19
  }
28
20
  function entries() {
29
- const entries = (0, node_fs_1.readdirSync)(ROOT, { withFileTypes: true });
21
+ const entries = readdirSync(ROOT, { withFileTypes: true });
30
22
  return entries.filter((entry) => entry.isDirectory());
31
23
  }
32
- const ROOT = (0, node_path_1.resolve)(__dirname, '../components/');
24
+ export function components() {
25
+ const labels = [];
26
+ const paths = [];
27
+ for (const entry of entries()) {
28
+ labels.push(entry.name.replace('.', '-'));
29
+ paths.push(resolve(ROOT, entry.name));
30
+ }
31
+ return { labels, paths };
32
+ }
33
+ const ROOT = resolve(import.meta.dirname, '../components/');
33
34
  //# sourceMappingURL=Composition.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Composition.js","sourceRoot":"","sources":["../source/Composition.ts"],"names":[],"mappings":";;;AAAA,qCAAkD;AAClD,yCAAmC;AACnC,uCAAwC;AAGxC,MAAa,WAAY,SAAQ,gBAAS;IACvB,IAAI,CAAY;IAEjC,YAAoB,IAAgB;QAClC,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;QACpB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;QAEtD,MAAM,WAAW,CAAC,OAAO,EAAE,CAAA;QAE3B,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAEzB,OAAO,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAA;IACvC,CAAC;IAEkB,OAAO;QACxB,OAAO,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAA;IAChD,CAAC;CACF;AAtBD,kCAsBC;AAED,SAAS,IAAI;IACX,OAAO,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAA,mBAAO,EAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;AAC5D,CAAC;AAED,SAAS,OAAO;IACd,MAAM,OAAO,GAAG,IAAA,qBAAW,EAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;IAE1D,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;AACvD,CAAC;AAED,MAAM,IAAI,GAAG,IAAA,mBAAO,EAAC,SAAS,EAAE,gBAAgB,CAAC,CAAA"}
1
+ {"version":3,"file":"Composition.js","sourceRoot":"","sources":["../source/Composition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAGxC,MAAM,OAAO,WAAY,SAAQ,SAAS;IACvB,IAAI,CAAM;IAE3B,YAAoB,IAAU;QAC5B,KAAK,EAAE,CAAA;QACP,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,MAAM,KAAK,GAAG,IAAI,EAAE,CAAA;QACpB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAA;QAEtD,MAAM,WAAW,CAAC,OAAO,EAAE,CAAA;QAE3B,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC3B,CAAC;CACF;AAED,MAAM,UAAU,IAAI;IAClB,OAAO,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;AAC5D,CAAC;AAED,SAAS,OAAO;IACd,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAA;IAE1D,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;AACvD,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;QACzC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IACvC,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;AAC1B,CAAC;AAOD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAA"}
@@ -1,9 +1,9 @@
1
1
  import { type extensions } from '@toa.io/core';
2
- import { Realtime } from './Realtime';
2
+ import { Realtime } from './Realtime.js';
3
3
  export declare class Factory implements extensions.Factory {
4
- private readonly boot;
5
- constructor(boot: Bootloader);
4
+ private readonly host;
5
+ constructor(host: Host);
6
6
  service(): Realtime;
7
7
  private discovery;
8
8
  }
9
- export type Bootloader = typeof import('@toa.io/boot');
9
+ export type Host = extensions.Host;
@@ -1,28 +1,23 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Factory = void 0;
4
- const core_1 = require("@toa.io/core");
5
- const Realtime_1 = require("./Realtime");
6
- const Composition_1 = require("./Composition");
7
- const Routes_1 = require("./Routes");
8
- class Factory {
9
- boot;
10
- constructor(boot) {
11
- this.boot = boot;
1
+ import { Locator } from '@toa.io/core';
2
+ import { Realtime } from './Realtime.js';
3
+ import { Composition } from './Composition.js';
4
+ import { Routes } from './Routes.js';
5
+ export class Factory {
6
+ host;
7
+ constructor(host) {
8
+ this.host = host;
12
9
  }
13
10
  service() {
14
- const discovery = this.discovery();
15
- const routes = new Routes_1.Routes(this.boot);
16
- const composition = new Composition_1.Composition(this.boot);
17
- const realtime = new Realtime_1.Realtime(routes, discovery);
11
+ const routes = new Routes(this.host);
12
+ const composition = new Composition(this.host);
13
+ const realtime = new Realtime(routes, async () => await this.discovery());
18
14
  realtime.depends(routes);
19
15
  realtime.depends(composition);
20
16
  return realtime;
21
17
  }
22
18
  async discovery() {
23
- const locator = new core_1.Locator('streams', 'realtime');
24
- return await this.boot.remote(locator);
19
+ const locator = new Locator('streams', 'realtime');
20
+ return await this.host.remote(locator, { service: 'realtime' });
25
21
  }
26
22
  }
27
- exports.Factory = Factory;
28
23
  //# sourceMappingURL=Factory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":";;;AAAA,uCAAuE;AACvE,yCAAqC;AACrC,+CAA2C;AAC3C,qCAAiC;AAEjC,MAAa,OAAO;IACD,IAAI,CAAY;IAEjC,YAAoB,IAAgB;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEM,OAAO;QACZ,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,CAAA;QAClC,MAAM,MAAM,GAAG,IAAI,eAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACpC,MAAM,WAAW,GAAG,IAAI,yBAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9C,MAAM,QAAQ,GAAG,IAAI,mBAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;QAEhD,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAE7B,OAAO,QAAQ,CAAA;IACjB,CAAC;IAEO,KAAK,CAAC,SAAS;QACrB,MAAM,OAAO,GAAG,IAAI,cAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;QAElD,OAAO,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACxC,CAAC;CACF;AAxBD,0BAwBC"}
1
+ {"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,OAAO,EAAE,MAAM,cAAc,CAAA;AACvE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,MAAM,OAAO,OAAO;IACD,IAAI,CAAM;IAE3B,YAAoB,IAAU;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;IAEM,OAAO;QACZ,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACpC,MAAM,WAAW,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9C,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,CAAA;QAEzE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAE7B,OAAO,QAAQ,CAAA;IACjB,CAAC;IAEO,KAAK,CAAC,SAAS;QACrB,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;QAElD,OAAO,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAA;IACjE,CAAC;CACF"}
@@ -1,10 +1,11 @@
1
1
  import { type Component, Connector } from '@toa.io/core';
2
- import { type Routes } from './Routes';
2
+ import { type Routes } from './Routes.js';
3
3
  export declare class Realtime extends Connector {
4
4
  private readonly discovery;
5
5
  private streams;
6
- constructor(routes: Routes, discovery: Promise<Component>);
6
+ constructor(routes: Routes, discovery: () => Promise<Component>);
7
7
  protected open(): Promise<void>;
8
8
  protected dispose(): void;
9
9
  private push;
10
+ private deliver;
10
11
  }
@@ -1,8 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Realtime = void 0;
4
- const core_1 = require("@toa.io/core");
5
- class Realtime extends core_1.Connector {
1
+ import { console, run } from 'openspan';
2
+ import { Connector } from '@toa.io/core';
3
+ export class Realtime extends Connector {
6
4
  discovery;
7
5
  streams = null;
8
6
  constructor(routes, discovery) {
@@ -11,17 +9,44 @@ class Realtime extends core_1.Connector {
11
9
  routes.events.on('data', this.push.bind(this));
12
10
  }
13
11
  async open() {
14
- this.streams = await this.discovery;
12
+ // the lookup belongs here, not in the constructor: dependencies connect before
13
+ // open, so the composition that serves realtime.streams is up to answer it. Asked
14
+ // any earlier the lookup goes unanswered, and it waits without a bound.
15
+ this.streams = await this.discovery();
15
16
  this.depends(this.streams);
16
17
  await this.streams.connect();
17
- console.log('Realtime has started.');
18
+ console.info('Realtime service started');
18
19
  }
19
20
  dispose() {
20
- console.log('Realtime shutdown complete.');
21
+ console.info('Realtime service shutdown complete');
21
22
  }
22
- push(event) {
23
- void this.streams?.invoke('push', { input: event });
23
+ push({ telemetry, ...event }) {
24
+ const processing = telemetry === null
25
+ ? this.deliver(event)
26
+ : run(telemetry, async () => await this.deliver(event));
27
+ void processing.catch((error) => console.error('Realtime push failed', error));
28
+ }
29
+ async deliver(event) {
30
+ /*
31
+ * The delivery span is created on behalf of the messaging destination
32
+ * (same as the core Receiver), so that service graphs display the fan-out:
33
+ * producer -> destination -> realtime
34
+ */
35
+ const delivery = {
36
+ name: `${event.event} deliver`,
37
+ kind: 'producer',
38
+ service: event.event,
39
+ attributes: { 'messaging.destination.name': event.event }
40
+ };
41
+ const options = {
42
+ name: `${event.event} push`,
43
+ kind: 'consumer',
44
+ service: 'realtime',
45
+ attributes: { 'messaging.destination.name': event.event }
46
+ };
47
+ await console.span(delivery, async () => await console.span(options, async () => {
48
+ await this.streams?.invoke('push', { input: event });
49
+ }));
24
50
  }
25
51
  }
26
- exports.Realtime = Realtime;
27
52
  //# sourceMappingURL=Realtime.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Realtime.js","sourceRoot":"","sources":["../source/Realtime.ts"],"names":[],"mappings":";;;AAAA,uCAAwD;AAGxD,MAAa,QAAS,SAAQ,gBAAS;IACpB,SAAS,CAAoB;IACtC,OAAO,GAAqB,IAAI,CAAA;IAExC,YAAoB,MAAc,EAAE,SAA6B;QAC/D,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAE1B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAChD,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,IAAI,CAAC,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAA;QACnC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAE1B,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAA;QAE5B,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAA;IACtC,CAAC;IAEkB,OAAO;QACxB,OAAO,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAA;IAC5C,CAAC;IAEO,IAAI,CAAE,KAAY;QACxB,KAAK,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;IACrD,CAAC;CACF;AA5BD,4BA4BC"}
1
+ {"version":3,"file":"Realtime.js","sourceRoot":"","sources":["../source/Realtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AACvC,OAAO,EAAkB,SAAS,EAAE,MAAM,cAAc,CAAA;AAIxD,MAAM,OAAO,QAAS,SAAQ,SAAS;IACpB,SAAS,CAA0B;IAC5C,OAAO,GAAqB,IAAI,CAAA;IAExC,YAAoB,MAAc,EAAE,SAAmC;QACrE,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAE1B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAChD,CAAC;IAEkB,KAAK,CAAC,IAAI;QAC3B,+EAA+E;QAC/E,kFAAkF;QAClF,wEAAwE;QACxE,IAAI,CAAC,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAA;QACrC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAE1B,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAA;QAE5B,OAAO,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;IAC1C,CAAC;IAEkB,OAAO;QACxB,OAAO,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAA;IACpD,CAAC;IAEO,IAAI,CAAE,EAAE,SAAS,EAAE,GAAG,KAAK,EAAQ;QACzC,MAAM,UAAU,GAAG,SAAS,KAAK,IAAI;YACnC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;YACrB,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAA;QAEzD,KAAK,UAAU,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC,CAAA;IAChF,CAAC;IAEO,KAAK,CAAC,OAAO,CAAE,KAA8B;QACnD;;;;WAIG;QACH,MAAM,QAAQ,GAAG;YACf,IAAI,EAAE,GAAG,KAAK,CAAC,KAAK,UAAU;YAC9B,IAAI,EAAE,UAAmB;YACzB,OAAO,EAAE,KAAK,CAAC,KAAK;YACpB,UAAU,EAAE,EAAE,4BAA4B,EAAE,KAAK,CAAC,KAAK,EAAE;SAC1D,CAAA;QAED,MAAM,OAAO,GAAG;YACd,IAAI,EAAE,GAAG,KAAK,CAAC,KAAK,OAAO;YAC3B,IAAI,EAAE,UAAmB;YACzB,OAAO,EAAE,UAAU;YACnB,UAAU,EAAE,EAAE,4BAA4B,EAAE,KAAK,CAAC,KAAK,EAAE;SAC1D,CAAA;QAED,MAAM,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;YAC9E,MAAM,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QACtD,CAAC,CAAC,CAAC,CAAA;IACL,CAAC;CACF"}
@@ -0,0 +1,25 @@
1
+ import { Connector, type Message } from '@toa.io/core';
2
+ import type { SpanContext } from 'openspan';
3
+ import type { Readable } from 'node:stream';
4
+ export declare class Receiver extends Connector {
5
+ private readonly event;
6
+ private readonly properties;
7
+ private readonly expose?;
8
+ private readonly stream;
9
+ constructor({ event, properties, stream, expose }: {
10
+ event: string;
11
+ properties: string[];
12
+ stream: Readable;
13
+ expose?: string[];
14
+ });
15
+ receive(message: Message<Record<string, string>>): Promise<void>;
16
+ private process;
17
+ private fit;
18
+ private push;
19
+ }
20
+ export interface Push {
21
+ key: string;
22
+ event: string;
23
+ data: Record<string, string>;
24
+ telemetry: SpanContext | null;
25
+ }
@@ -0,0 +1,55 @@
1
+ import { console, decode, run } from 'openspan';
2
+ import { Connector } from '@toa.io/core';
3
+ export class Receiver extends Connector {
4
+ event;
5
+ properties;
6
+ expose;
7
+ stream;
8
+ constructor({ event, properties, stream, expose }) {
9
+ super();
10
+ this.event = event;
11
+ this.properties = properties;
12
+ this.expose = expose;
13
+ this.stream = stream;
14
+ }
15
+ async receive(message) {
16
+ // the push continues the trace from the producer
17
+ const telemetry = message.telemetry === undefined ? null : decode(message.telemetry);
18
+ if (telemetry === null)
19
+ this.process(message, telemetry);
20
+ else
21
+ run(telemetry, () => this.process(message, telemetry));
22
+ }
23
+ process(message, telemetry) {
24
+ const data = this.fit(message.payload);
25
+ for (const property of this.properties) {
26
+ const key = message.payload[property];
27
+ if (key === undefined) {
28
+ console.debug('Event does not contain key property', { property, event: this.event });
29
+ continue;
30
+ }
31
+ if (Array.isArray(key))
32
+ // eslint-disable-next-line max-depth
33
+ for (const k of key)
34
+ this.push(k, data, telemetry);
35
+ else
36
+ this.push(key, data, telemetry);
37
+ }
38
+ }
39
+ fit(payload) {
40
+ if (this.expose === undefined)
41
+ return payload;
42
+ const entries = Object.entries(payload)
43
+ .filter(([key]) => this.expose.includes(key));
44
+ return Object.fromEntries(entries);
45
+ }
46
+ push(key, data, telemetry) {
47
+ if (key === null || typeof key === 'undefined') {
48
+ console.debug('Key is null or undefined, skipping', { key, event: this.event });
49
+ return;
50
+ }
51
+ console.debug('Pushing event to stream', { key, event: this.event, data });
52
+ this.stream.push({ key, event: this.event, data, telemetry });
53
+ }
54
+ }
55
+ //# sourceMappingURL=Receiver.js.map