@toa.io/extensions.realtime 1.0.0-alpha.21 → 1.0.0-alpha.212

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 (64) hide show
  1. package/components/streams/manifest.toa.yaml +12 -1
  2. package/components/streams/operations/create.d.ts +6 -2
  3. package/components/streams/operations/create.js +47 -14
  4. package/components/streams/operations/create.js.map +1 -1
  5. package/components/streams/operations/lib/Stash.d.ts +20 -0
  6. package/components/streams/operations/lib/Stash.js +71 -0
  7. package/components/streams/operations/lib/Stash.js.map +1 -0
  8. package/components/streams/operations/lib/Stream.d.ts +11 -0
  9. package/components/streams/operations/lib/Stream.js +34 -0
  10. package/components/streams/operations/lib/Stream.js.map +1 -0
  11. package/components/streams/operations/lib/index.d.ts +2 -0
  12. package/components/streams/operations/lib/index.js +8 -0
  13. package/components/streams/operations/lib/index.js.map +1 -0
  14. package/components/streams/operations/lib/types.d.ts +22 -0
  15. package/components/streams/operations/lib/types.js.map +1 -0
  16. package/components/streams/operations/push.d.ts +1 -1
  17. package/components/streams/operations/push.js +6 -0
  18. package/components/streams/operations/push.js.map +1 -1
  19. package/components/streams/operations/tsconfig.tsbuildinfo +1 -1
  20. package/components/streams/source/create.ts +62 -12
  21. package/components/streams/source/lib/Stash.ts +105 -0
  22. package/components/streams/source/lib/Stream.ts +40 -0
  23. package/components/streams/source/lib/index.ts +2 -0
  24. package/components/streams/source/lib/types.ts +24 -0
  25. package/components/streams/source/push.ts +8 -1
  26. package/features/static.feature +89 -0
  27. package/features/steps/Realtime.ts +2 -2
  28. package/features/steps/Streams.ts +44 -9
  29. package/features/steps/components/messages/manifest.toa.yaml +2 -0
  30. package/package.json +6 -2
  31. package/readme.md +32 -6
  32. package/source/Composition.ts +1 -7
  33. package/source/Realtime.ts +4 -2
  34. package/source/Receiver.ts +49 -0
  35. package/source/Routes.ts +7 -27
  36. package/source/extension.ts +69 -0
  37. package/source/index.ts +1 -0
  38. package/transpiled/Composition.d.ts +1 -1
  39. package/transpiled/Composition.js +2 -5
  40. package/transpiled/Composition.js.map +1 -1
  41. package/transpiled/Realtime.js +5 -3
  42. package/transpiled/Realtime.js.map +1 -1
  43. package/transpiled/Receiver.d.ts +11 -0
  44. package/transpiled/Receiver.js +41 -0
  45. package/transpiled/Receiver.js.map +1 -0
  46. package/transpiled/Routes.d.ts +1 -2
  47. package/transpiled/Routes.js +5 -17
  48. package/transpiled/Routes.js.map +1 -1
  49. package/transpiled/extension.d.ts +8 -0
  50. package/transpiled/extension.js +46 -0
  51. package/transpiled/extension.js.map +1 -0
  52. package/transpiled/index.d.ts +1 -0
  53. package/transpiled/index.js +15 -0
  54. package/transpiled/index.js.map +1 -1
  55. package/transpiled/tsconfig.tsbuildinfo +1 -1
  56. package/components/streams/operations/lib/stream.d.ts +0 -10
  57. package/components/streams/operations/lib/stream.js +0 -31
  58. package/components/streams/operations/lib/stream.js.map +0 -1
  59. package/components/streams/operations/types.d.ts +0 -11
  60. package/components/streams/operations/types.js.map +0 -1
  61. package/components/streams/source/lib/stream.ts +0 -37
  62. package/components/streams/source/types.ts +0 -13
  63. package/stage/streams.test.ts +0 -128
  64. /package/components/streams/operations/{types.js → lib/types.js} +0 -0
@@ -0,0 +1,105 @@
1
+ import type { Redis } from 'ioredis'
2
+
3
+ export class Stash {
4
+ private readonly stash: Redis
5
+ private readonly configuration: Configuration
6
+
7
+ public constructor (stash: any, configuration: Configuration) {
8
+ this.stash = stash
9
+ this.configuration = configuration
10
+ }
11
+
12
+ public async connect (key: string): Promise<string | Error> {
13
+ return await this.xadd(key, 'connect')
14
+ }
15
+
16
+ public async push (key: string, event: string, data: unknown): Promise<string | Error> {
17
+ return await this.xadd(key, event, data)
18
+ }
19
+
20
+ public async pop (key: string, token: string): Promise<[string, Event[]] | null | Error> {
21
+ const stamp = this.decode(token)
22
+
23
+ const results = await this.stash
24
+ .xread('STREAMS', key, stamp)
25
+ .catch((error: Error) => error)
26
+
27
+ if (results === null)
28
+ return ERR_NO_RESULTS
29
+
30
+ if (results instanceof Error)
31
+ return results
32
+
33
+ if (results.length === 0)
34
+ return null
35
+
36
+ const [, items] = results[0]
37
+ const events: Event[] = []
38
+
39
+ let lastStamp: string | null = null
40
+
41
+ for (const item of items) {
42
+ const [, event, , json] = item[1]
43
+
44
+ lastStamp = item[0]
45
+
46
+ events.push({ event, data: JSON.parse(json) })
47
+ }
48
+
49
+ if (lastStamp === null)
50
+ return null
51
+
52
+ return [this.encode(lastStamp), events]
53
+ }
54
+
55
+ private async xadd (key: string, event: string, data?: unknown): Promise<string | Error> {
56
+ const args = ['MAXLEN', '~', this.configuration.maxlen, '*', 'type', event]
57
+
58
+ if (data !== undefined)
59
+ args.push('data', JSON.stringify(data))
60
+
61
+ const results = await this.stash
62
+ .multi()
63
+ .xadd(key, ...args)
64
+ .expire(key, this.configuration.expire)
65
+ .exec()
66
+ .catch((error: Error) => error)
67
+
68
+ if (results === null)
69
+ return ERR_NO_RESULTS
70
+
71
+ if (results instanceof Error)
72
+ return results
73
+
74
+ const [[error, stamp]] = results
75
+
76
+ if (error !== null)
77
+ return error
78
+
79
+ return this.encode(stamp as string)
80
+ }
81
+
82
+ private encode (token: string): string {
83
+ return Buffer.from(token).toString('base64url')
84
+ }
85
+
86
+ private decode (token: string): string {
87
+ return Buffer.from(token, 'base64url').toString()
88
+ }
89
+ }
90
+
91
+ interface Configuration {
92
+ maxlen: number
93
+ expire: number
94
+ }
95
+
96
+ interface Event {
97
+ event: string
98
+ data: unknown
99
+ }
100
+
101
+ class NoResultsError extends Error {
102
+ public readonly code = 'NO_RESULTS'
103
+ }
104
+
105
+ const ERR_NO_RESULTS = new NoResultsError()
@@ -0,0 +1,40 @@
1
+ import { EventEmitter, Readable } from 'node:stream'
2
+
3
+ export class Stream extends Readable {
4
+ public events = new EventEmitter()
5
+
6
+ private interval: NodeJS.Timeout | null = null
7
+
8
+ public constructor () {
9
+ super(objectMode)
10
+ }
11
+
12
+ // has to be here
13
+ public override _read (): void {
14
+ if (this.interval === null)
15
+ this.interval = setInterval(() => this.heartbeat(), HEARTBEAT_INTERVAL)
16
+ }
17
+
18
+ public override _destroy (error: Error | null, callback: (error?: (Error | null)) => void): void {
19
+ if (this.interval !== null)
20
+ clearInterval(this.interval)
21
+
22
+ this.events.emit('destroy')
23
+
24
+ super._destroy(error, callback)
25
+ }
26
+
27
+ public heartbeat (stream: Readable = this): boolean {
28
+ const resume = stream.push('heartbeat ' + Date.now())
29
+
30
+ if (!resume && this.interval !== null) {
31
+ clearInterval(this.interval)
32
+ this.interval = null
33
+ }
34
+
35
+ return resume
36
+ }
37
+ }
38
+
39
+ const HEARTBEAT_INTERVAL = 16_000 // why?
40
+ const objectMode = { objectMode: true }
@@ -0,0 +1,2 @@
1
+ export { Stash } from './Stash'
2
+ export { Stream } from './Stream'
@@ -0,0 +1,24 @@
1
+ import { type Stream } from './Stream'
2
+ import { type Stash } from './Stash'
3
+
4
+ export interface Context {
5
+ stash: any
6
+ state: {
7
+ streams: Map<string, Stream>
8
+ stash: Stash
9
+ }
10
+ logs: {
11
+ info: (m: string, att?: object) => void
12
+ error: (m: string, att?: object) => void
13
+ }
14
+ configuration: {
15
+ maxlen: number
16
+ expire: number
17
+ }
18
+ }
19
+
20
+ export interface PushInput {
21
+ key: string
22
+ event: string
23
+ data: unknown
24
+ }
@@ -1,5 +1,12 @@
1
- import { type Context, type PushInput } from './types'
1
+ import { type Context, type PushInput } from './lib/types'
2
2
 
3
3
  export async function effect ({ key, event, data }: PushInput, context: Context): Promise<void> {
4
4
  context.state.streams.get(key)?.push({ event, data })
5
+
6
+ void context.state.stash.push(key, event, data).then((token) => {
7
+ if (token instanceof Error)
8
+ context.logs.error('Failed to push to stash', { key, error: token })
9
+ else
10
+ context.state.streams.get(key)?.push({ event: 'token', data: token })
11
+ })
5
12
  }
@@ -1,5 +1,12 @@
1
1
  Feature: Static routes
2
2
 
3
+ Scenario: Debug
4
+ Given the `messages` component is running with routes:
5
+ """yaml
6
+ created: [sender, recipient]
7
+ """
8
+ And the stream `004e02a959c04cecaf111827f91caa36` is consumed
9
+
3
10
  Scenario: Routing an event
4
11
  Given the `messages` component is running with routes:
5
12
  """yaml
@@ -30,3 +37,85 @@ Feature: Static routes
30
37
  recipient: 004e02a959c04cecaf111827f91caa36
31
38
  text: Hello!
32
39
  """
40
+
41
+ Scenario: Routing an event using array
42
+ Given the `messages` component is running with routes:
43
+ """yaml
44
+ created: watchers
45
+ """
46
+ And the stream `51c15a7290ce47e0af8ec41d60dccb32` is consumed
47
+ And the stream `bb27366509a64178a39313aac42435ae` is consumed
48
+ When the `messages.create` is called with:
49
+ """yaml
50
+ input:
51
+ watchers:
52
+ - 51c15a7290ce47e0af8ec41d60dccb32
53
+ - bb27366509a64178a39313aac42435ae
54
+ sender: 96db5a47a8244eb3b21820781b7d596e
55
+ recipient: 004e02a959c04cecaf111827f91caa36
56
+ text: Hello!
57
+ """
58
+ Then an event is received from the stream `51c15a7290ce47e0af8ec41d60dccb32`:
59
+ """yaml
60
+ event: default.messages.created
61
+ data:
62
+ sender: 96db5a47a8244eb3b21820781b7d596e
63
+ recipient: 004e02a959c04cecaf111827f91caa36
64
+ text: Hello!
65
+ """
66
+ And an event is received from the stream `bb27366509a64178a39313aac42435ae`:
67
+ """yaml
68
+ event: default.messages.created
69
+ data:
70
+ sender: 96db5a47a8244eb3b21820781b7d596e
71
+ recipient: 004e02a959c04cecaf111827f91caa36
72
+ text: Hello!
73
+ """
74
+
75
+ Scenario: Continuation token
76
+ Given the `messages` component is running with routes:
77
+ """yaml
78
+ created: [sender, recipient]
79
+ """
80
+ And the stream `004e02a959c04cecaf111827f91caa36` is consumed
81
+ And an event is received from the stream `004e02a959c04cecaf111827f91caa36`:
82
+ """yaml
83
+ event: token
84
+ """
85
+
86
+ When the consumer `004e02a959c04cecaf111827f91caa36` is disconnected
87
+ And the `messages.create` is called with:
88
+ """yaml
89
+ input:
90
+ sender: 96db5a47a8244eb3b21820781b7d596e
91
+ recipient: 004e02a959c04cecaf111827f91caa36
92
+ text: Hello!
93
+ """
94
+ And the `messages.create` is called with:
95
+ """yaml
96
+ input:
97
+ sender: 004e02a959c04cecaf111827f91caa36
98
+ recipient: 96db5a47a8244eb3b21820781b7d596e
99
+ text: Hi!
100
+ """
101
+ And the consumer `004e02a959c04cecaf111827f91caa36` is reconnected
102
+ Then an event is received from the stream `004e02a959c04cecaf111827f91caa36`:
103
+ """yaml
104
+ event: default.messages.created
105
+ data:
106
+ sender: 96db5a47a8244eb3b21820781b7d596e
107
+ recipient: 004e02a959c04cecaf111827f91caa36
108
+ text: Hello!
109
+ """
110
+ And an event is received from the stream `004e02a959c04cecaf111827f91caa36`:
111
+ """yaml
112
+ event: default.messages.created
113
+ data:
114
+ sender: 004e02a959c04cecaf111827f91caa36
115
+ recipient: 96db5a47a8244eb3b21820781b7d596e
116
+ text: Hi!
117
+ """
118
+ And an event is received from the stream `004e02a959c04cecaf111827f91caa36`:
119
+ """yaml
120
+ event: token
121
+ """
@@ -2,7 +2,7 @@ import * as boot from '@toa.io/boot'
2
2
  import { encode } from '@toa.io/generic'
3
3
  import { type Connector } from '@toa.io/core'
4
4
  import { after, binding } from 'cucumber-tsflow'
5
- import { Factory } from '../../source/Factory'
5
+ import { Factory } from '../../source'
6
6
 
7
7
  @binding()
8
8
  export class Realtime {
@@ -15,7 +15,7 @@ export class Realtime {
15
15
  }
16
16
 
17
17
  @after()
18
- private async shutdown (): Promise<void> {
18
+ public async shutdown (): Promise<void> {
19
19
  this.connected = false
20
20
 
21
21
  await this.service.disconnect()
@@ -1,35 +1,38 @@
1
- import { type Readable } from 'node:stream'
1
+ import * as assert from 'node:assert'
2
+ import { setTimeout } from 'node:timers/promises'
2
3
  import { after, binding, given, then } from 'cucumber-tsflow'
3
4
  import { match } from '@toa.io/generic'
4
-
5
- import { type Component } from '@toa.io/core'
6
5
  import { parse } from '@toa.io/yaml'
7
6
  import * as stage from '@toa.io/userland/stage'
8
7
  import { Realtime } from './Realtime'
8
+ import type { Readable } from 'node:stream'
9
+ import type { Component } from '@toa.io/core'
9
10
 
10
11
  @binding([Realtime])
11
12
  export class Streams {
12
13
  private readonly realtime: Realtime
13
14
  private remote: Component | null = null
14
15
  private streams: Record<string, Readable> = {}
15
- private events: Record<string, object[]> = {}
16
+ private events: Record<string, Event[]> = {}
16
17
 
17
18
  public constructor (realtime: Realtime) {
18
19
  this.realtime = realtime
19
20
  }
20
21
 
21
- @given('the stream `{word}` is consumed')
22
+ @given('the stream `{word}` is consumed', { timeout: 30_000 })
22
23
  public async consume (key: string): Promise<void> {
23
24
  await this.realtime.serve()
24
25
 
25
26
  this.remote ??= await stage.remote('realtime.streams')
26
27
  this.events[key] = []
27
- this.streams[key] = await this.remote.invoke('create', { input: { key } })
28
- this.streams[key].on('data', (data: object) => this.events[key].push(data))
28
+
29
+ await this.createStream(key)
29
30
  }
30
31
 
31
32
  @then('an event is received from the stream `{word}`:')
32
- public received (key: string, yaml: string): void {
33
+ public async received (key: string, yaml: string): Promise<void> {
34
+ await setTimeout(100)
35
+
33
36
  const expected = parse<object>(yaml)
34
37
 
35
38
  for (const event of this.events[key])
@@ -39,12 +42,44 @@ export class Streams {
39
42
  throw new Error('No matching event received')
40
43
  }
41
44
 
45
+ @then('the consumer `{word}` is disconnected')
46
+ public disconnected (key: string): void {
47
+ this.streams[key]?.destroy()
48
+ delete this.streams[key]
49
+ }
50
+
51
+ @then('the consumer `{word}` is reconnected')
52
+ public async reconnected (key: string): Promise<void> {
53
+ const last = this.events[key].findLast((event) => event.event === 'token')
54
+
55
+ assert.ok(last, `No last event found for stream ${key}`)
56
+
57
+ await this.createStream(key, last.data as string)
58
+ }
59
+
42
60
  @after()
43
- private shutdown (): void {
61
+ public async shutdown (): Promise<void> {
44
62
  for (const stream of Object.values(this.streams))
45
63
  stream.destroy()
46
64
 
47
65
  this.streams = {}
48
66
  this.events = {}
67
+
68
+ await setTimeout(100)
49
69
  }
70
+
71
+ private async createStream (key: string, token?: string): Promise<void> {
72
+ this.streams[key] = await this.remote!.invoke('create', { input: { key, token } })
73
+ this.streams[key].on('data', (event: Event) => {
74
+ console.log('[TEST] Received event', event)
75
+ this.events[key]?.push(event)
76
+ })
77
+ }
78
+ }
79
+
80
+ interface Event {
81
+ key: string
82
+ token: string
83
+ event: string
84
+ data?: unknown
50
85
  }
@@ -5,6 +5,7 @@ entity:
5
5
  sender*: string(32)
6
6
  recipient*: string(32)
7
7
  text*: string
8
+ watchers: [string(32)]
8
9
 
9
10
  operations:
10
11
  create:
@@ -14,3 +15,4 @@ operations:
14
15
  sender*: .
15
16
  recipient*: .
16
17
  text*: .
18
+ watchers: .
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.realtime",
3
- "version": "1.0.0-alpha.21",
3
+ "version": "1.0.0-alpha.212",
4
4
  "description": "Toa Realtime",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -16,6 +16,10 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
+ "dependencies": {
20
+ "@toa.io/core": "1.0.0-alpha.212",
21
+ "openspan": "1.0.0-alpha.173"
22
+ },
19
23
  "jest": {
20
24
  "preset": "ts-jest",
21
25
  "testEnvironment": "node"
@@ -24,5 +28,5 @@
24
28
  "transpile": "npx tsc && npx tsc -p ./components/streams",
25
29
  "features": "npx cucumber-js"
26
30
  },
27
- "gitHead": "da9f4c278f6ab02a28f65c6e25c713c013cbfce9"
31
+ "gitHead": "d8aefb3b37df15be74bb63e1447f76c27e88d9be"
28
32
  }
package/readme.md CHANGED
@@ -10,17 +10,20 @@
10
10
  </a>
11
11
 
12
12
  Realtime extension combines application events into streams according to defined routes.
13
- Clients may consume these streams [via Exposition](#exposition).
13
+ Clients may consume these streams [via Exposition](/extensions/exposition).
14
+
15
+ If stream is idle for 16 seconds, a `heartbeat` message is sent.
14
16
 
15
17
  ## Static routes
16
18
 
17
19
  Static route specifies an event that should be combined into a stream using specified property of
18
- event's payload as a stream key.
20
+ event's payload as a stream key or an array of stream keys.
19
21
 
20
22
  Static routes may be defined in Component manifest or the Context annotation.
21
23
 
22
24
  ```yaml
23
25
  # manifest.toa.yaml
26
+
24
27
  name: users
25
28
 
26
29
  realtime:
@@ -29,13 +32,24 @@ realtime:
29
32
 
30
33
  ```yaml
31
34
  # context.toa.yaml
35
+
32
36
  realtime:
33
37
  users.updated: id
34
- orders.created: custromer_id
38
+ orders.created: customer_id
35
39
  ```
36
40
 
37
41
  In case of conflict, the Context annotation takes precedence.
38
42
 
43
+ Multiple stream keys may be defined for a single event.
44
+
45
+ ```yaml
46
+ # manifest.toa.yaml
47
+ name: messages
48
+
49
+ realtime:
50
+ updated: [sender_id, recipient_id]
51
+ ```
52
+
39
53
  ### Static route examples
40
54
 
41
55
  Given two rules: `users.updated: id` and `orders.created: customer_id`,
@@ -92,8 +106,20 @@ Realtime extension, and are
92
106
  accessible via the `/realtime/streams/:key/` resource with
93
107
  the [`auth:id: key`](/extensions/exposition/documentation/access.md#id) authorization rule.
94
108
 
95
- Refer to the [Exposition extension](/extensions/exposition) for more
96
- details:
109
+ Refer to the [Exposition extension](/extensions/exposition) for more details:
97
110
 
98
- - [Streams](/extensions/exposition/documentation/protocol.md#streams)
111
+ - [Multipart responses](/extensions/exposition/documentation/protocol.md#multipart-types)
99
112
  - [Access authorization](/extensions/exposition/documentation/access.md)
113
+
114
+ ## Resources management
115
+
116
+ Resource requests and limits can be specified by `resources` annotation:
117
+
118
+ ```yaml
119
+ # context.toa.yaml
120
+
121
+ realtime:
122
+ resources:
123
+ cpu: [100m, 500m]
124
+ memory: [100Mi, 200Mi]
125
+ ```
@@ -18,16 +18,10 @@ export class Composition extends Connector {
18
18
  await composition.connect()
19
19
 
20
20
  this.depends(composition)
21
-
22
- console.info('Composition complete.')
23
- }
24
-
25
- protected override dispose (): void {
26
- console.info('Composition shutdown complete.')
27
21
  }
28
22
  }
29
23
 
30
- function find (): string[] {
24
+ export function find (): string[] {
31
25
  return entries().map((entry) => resolve(ROOT, entry.name))
32
26
  }
33
27
 
@@ -1,3 +1,4 @@
1
+ import { console } from 'openspan'
1
2
  import { type Component, Connector } from '@toa.io/core'
2
3
  import { type Routes } from './Routes'
3
4
 
@@ -19,15 +20,16 @@ export class Realtime extends Connector {
19
20
 
20
21
  await this.streams.connect()
21
22
 
22
- console.log('Realtime has started.')
23
+ console.info('Realtime service started')
23
24
  }
24
25
 
25
26
  protected override dispose (): void {
26
- console.log('Realtime shutdown complete.')
27
+ console.info('Realtime service shutdown complete')
27
28
  }
28
29
 
29
30
  private push (event: Event): void {
30
31
  void this.streams?.invoke('push', { input: event })
32
+ .catch((error) => console.error('Realtime push failed', error))
31
33
  }
32
34
  }
33
35
 
@@ -0,0 +1,49 @@
1
+ import { console } from 'openspan'
2
+ import { Connector, type Message } from '@toa.io/core'
3
+ import type { Readable } from 'node:stream'
4
+
5
+ export class Receiver extends Connector {
6
+ private readonly event: string
7
+ private readonly properties: string[]
8
+ private readonly stream: Readable
9
+
10
+ public constructor (event: string, properties: string[], stream: Readable) {
11
+ super()
12
+
13
+ this.event = event
14
+ this.properties = properties
15
+ this.stream = stream
16
+ }
17
+
18
+ public receive (message: Message<Record<string, string>>): void {
19
+ for (const property of this.properties) {
20
+ const key = message.payload[property]
21
+
22
+ if (key === undefined) {
23
+ console.debug('Event does not contain key property',
24
+ { property, event: this.event })
25
+
26
+ continue
27
+ }
28
+
29
+ if (Array.isArray(key))
30
+ // eslint-disable-next-line max-depth
31
+ for (const k of key as string[])
32
+ this.push(k, message.payload)
33
+ else
34
+ this.push(key, message.payload)
35
+ }
36
+ }
37
+
38
+ private push (key: string | null, data: Record<string, string>): void {
39
+ if (key === null || typeof key === 'undefined') {
40
+ console.debug('Key is null or undefined, skipping', { key, event: this.event })
41
+
42
+ return
43
+ }
44
+
45
+ console.debug('Pushing event to stream', { key, event: this.event, data })
46
+
47
+ this.stream.push({ key, event: this.event, data })
48
+ }
49
+ }
package/source/Routes.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { Readable } from 'node:stream'
2
- import { Connector, type Message } from '@toa.io/core'
2
+ import { console } from 'openspan'
3
+ import { Connector } from '@toa.io/core'
3
4
  import { decode } from '@toa.io/generic'
4
- import { type Bootloader } from './Factory'
5
+ import { Receiver } from './Receiver'
6
+ import type { Bootloader } from './Factory'
5
7
 
6
8
  export class Routes extends Connector {
7
9
  public events = new Events()
@@ -26,7 +28,7 @@ export class Routes extends Connector {
26
28
  const creating = []
27
29
 
28
30
  for (const { event, properties } of routes) {
29
- const consumer = this.boot.receive(event, this.getReceiver(event, properties))
31
+ const consumer = this.boot.receive(event, new Receiver(event, properties, this.events))
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
 
@@ -78,7 +62,3 @@ export interface Route {
78
62
  event: string
79
63
  properties: string[]
80
64
  }
81
-
82
- interface Receiver {
83
- receive: (message: Message<Record<string, string>>) => void
84
- }