@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.
- package/CHANGELOG.md +337 -0
- package/components/realtime.streams/manifest.toa.yaml +47 -0
- package/components/{streams → realtime.streams}/operations/create.d.ts +7 -4
- package/components/realtime.streams/operations/create.js +65 -0
- package/components/realtime.streams/operations/create.js.map +1 -0
- package/components/realtime.streams/operations/lib/Stash.d.ts +21 -0
- package/components/realtime.streams/operations/lib/Stash.js +86 -0
- package/components/realtime.streams/operations/lib/Stash.js.map +1 -0
- package/components/realtime.streams/operations/lib/Stream.d.ts +13 -0
- package/components/realtime.streams/operations/lib/Stream.js +43 -0
- package/components/realtime.streams/operations/lib/Stream.js.map +1 -0
- package/components/realtime.streams/operations/lib/index.d.ts +2 -0
- package/components/realtime.streams/operations/lib/index.js +3 -0
- package/components/realtime.streams/operations/lib/index.js.map +1 -0
- package/components/{streams → realtime.streams}/operations/push.d.ts +1 -1
- package/components/realtime.streams/operations/push.js +10 -0
- package/components/realtime.streams/operations/push.js.map +1 -0
- package/components/realtime.streams/source/create.ts +97 -0
- package/components/realtime.streams/source/lib/Stash.ts +133 -0
- package/components/realtime.streams/source/lib/Stream.ts +56 -0
- package/components/realtime.streams/source/lib/index.ts +2 -0
- package/components/realtime.streams/source/push.ts +12 -0
- package/components/{streams → realtime.streams}/tsconfig.json +1 -0
- package/components/realtime.streams/tsconfig.tsbuildinfo +1 -0
- package/components/realtime.streams/types/index.d.ts +20 -0
- package/components/realtime.streams/types/toa.d.ts +29 -0
- package/{components/context.toa.yaml → context.toa.yaml} +0 -1
- package/cucumber.mjs +5 -0
- package/features/static.feature +120 -0
- package/features/steps/Components.ts +14 -14
- package/features/steps/Realtime.ts +14 -18
- package/features/steps/Streams.ts +51 -13
- package/features/steps/components/messages/manifest.toa.yaml +37 -6
- package/features/steps/config.ts +12 -0
- package/package.json +8 -7
- package/readme.md +33 -7
- package/source/Composition.ts +24 -13
- package/source/Factory.ts +11 -13
- package/source/Realtime.ts +41 -13
- package/source/Receiver.ts +86 -0
- package/source/Routes.ts +14 -37
- package/source/extension.ts +82 -0
- package/source/index.ts +2 -1
- package/transpiled/Composition.d.ts +10 -4
- package/transpiled/Composition.js +21 -20
- package/transpiled/Composition.js.map +1 -1
- package/transpiled/Factory.d.ts +4 -4
- package/transpiled/Factory.js +13 -18
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Realtime.d.ts +3 -2
- package/transpiled/Realtime.js +36 -11
- package/transpiled/Realtime.js.map +1 -1
- package/transpiled/Receiver.d.ts +25 -0
- package/transpiled/Receiver.js +55 -0
- package/transpiled/Receiver.js.map +1 -0
- package/transpiled/Routes.d.ts +5 -10
- package/transpiled/Routes.js +14 -31
- package/transpiled/Routes.js.map +1 -1
- package/transpiled/extension.d.ts +19 -0
- package/transpiled/extension.js +48 -0
- package/transpiled/extension.js.map +1 -0
- package/transpiled/index.d.ts +2 -1
- package/transpiled/index.js +2 -5
- package/transpiled/index.js.map +1 -1
- package/tsconfig.json +2 -1
- package/tsconfig.tsbuildinfo +1 -0
- package/components/streams/manifest.toa.yaml +0 -21
- package/components/streams/operations/create.js +0 -29
- package/components/streams/operations/create.js.map +0 -1
- package/components/streams/operations/lib/stream.d.ts +0 -10
- package/components/streams/operations/lib/stream.js +0 -31
- package/components/streams/operations/lib/stream.js.map +0 -1
- package/components/streams/operations/push.js +0 -8
- package/components/streams/operations/push.js.map +0 -1
- package/components/streams/operations/tsconfig.tsbuildinfo +0 -1
- package/components/streams/operations/types.d.ts +0 -11
- package/components/streams/operations/types.js +0 -3
- package/components/streams/operations/types.js.map +0 -1
- package/components/streams/source/create.ts +0 -38
- package/components/streams/source/lib/stream.ts +0 -37
- package/components/streams/source/push.ts +0 -5
- package/components/streams/source/types.ts +0 -13
- package/cucumber.js +0 -8
- package/stage/streams.test.ts +0 -128
- package/transpiled/tsconfig.tsbuildinfo +0 -1
package/cucumber.mjs
ADDED
package/features/static.feature
CHANGED
|
@@ -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,116 @@ Feature: Static routes
|
|
|
30
37
|
recipient: 004e02a959c04cecaf111827f91caa36
|
|
31
38
|
text: Hello!
|
|
32
39
|
"""
|
|
40
|
+
|
|
41
|
+
Scenario: Exposing event properties
|
|
42
|
+
Given the `messages` component is running with routes:
|
|
43
|
+
"""yaml
|
|
44
|
+
created:
|
|
45
|
+
key: [sender, recipient]
|
|
46
|
+
expose: [text, sender]
|
|
47
|
+
"""
|
|
48
|
+
And the stream `004e02a959c04cecaf111827f91caa36` is consumed
|
|
49
|
+
And the stream `96db5a47a8244eb3b21820781b7d596e` is consumed
|
|
50
|
+
When the `messages.create` is called with:
|
|
51
|
+
"""yaml
|
|
52
|
+
input:
|
|
53
|
+
sender: 96db5a47a8244eb3b21820781b7d596e
|
|
54
|
+
recipient: 004e02a959c04cecaf111827f91caa36
|
|
55
|
+
text: Hello!
|
|
56
|
+
"""
|
|
57
|
+
Then an event is received from the stream `004e02a959c04cecaf111827f91caa36`:
|
|
58
|
+
"""yaml
|
|
59
|
+
event: default.messages.created
|
|
60
|
+
data:
|
|
61
|
+
sender: 96db5a47a8244eb3b21820781b7d596e
|
|
62
|
+
text: Hello!
|
|
63
|
+
"""
|
|
64
|
+
And an event is received from the stream `96db5a47a8244eb3b21820781b7d596e`:
|
|
65
|
+
"""yaml
|
|
66
|
+
event: default.messages.created
|
|
67
|
+
data:
|
|
68
|
+
sender: 96db5a47a8244eb3b21820781b7d596e
|
|
69
|
+
text: Hello!
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
Scenario: Routing an event using array
|
|
73
|
+
Given the `messages` component is running with routes:
|
|
74
|
+
"""yaml
|
|
75
|
+
created: watchers
|
|
76
|
+
"""
|
|
77
|
+
And the stream `51c15a7290ce47e0af8ec41d60dccb32` is consumed
|
|
78
|
+
And the stream `bb27366509a64178a39313aac42435ae` is consumed
|
|
79
|
+
When the `messages.create` is called with:
|
|
80
|
+
"""yaml
|
|
81
|
+
input:
|
|
82
|
+
watchers:
|
|
83
|
+
- 51c15a7290ce47e0af8ec41d60dccb32
|
|
84
|
+
- bb27366509a64178a39313aac42435ae
|
|
85
|
+
sender: 96db5a47a8244eb3b21820781b7d596e
|
|
86
|
+
recipient: 004e02a959c04cecaf111827f91caa36
|
|
87
|
+
text: Hello!
|
|
88
|
+
"""
|
|
89
|
+
Then an event is received from the stream `51c15a7290ce47e0af8ec41d60dccb32`:
|
|
90
|
+
"""yaml
|
|
91
|
+
event: default.messages.created
|
|
92
|
+
data:
|
|
93
|
+
sender: 96db5a47a8244eb3b21820781b7d596e
|
|
94
|
+
recipient: 004e02a959c04cecaf111827f91caa36
|
|
95
|
+
text: Hello!
|
|
96
|
+
"""
|
|
97
|
+
And an event is received from the stream `bb27366509a64178a39313aac42435ae`:
|
|
98
|
+
"""yaml
|
|
99
|
+
event: default.messages.created
|
|
100
|
+
data:
|
|
101
|
+
sender: 96db5a47a8244eb3b21820781b7d596e
|
|
102
|
+
recipient: 004e02a959c04cecaf111827f91caa36
|
|
103
|
+
text: Hello!
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
Scenario: Continuation token
|
|
107
|
+
Given the `messages` component is running with routes:
|
|
108
|
+
"""yaml
|
|
109
|
+
created: [sender, recipient]
|
|
110
|
+
"""
|
|
111
|
+
And the stream `004e02a959c04cecaf111827f91caa36` is consumed
|
|
112
|
+
And an event is received from the stream `004e02a959c04cecaf111827f91caa36`:
|
|
113
|
+
"""yaml
|
|
114
|
+
event: token
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
When the consumer `004e02a959c04cecaf111827f91caa36` is disconnected
|
|
118
|
+
And the `messages.create` is called with:
|
|
119
|
+
"""yaml
|
|
120
|
+
input:
|
|
121
|
+
sender: 96db5a47a8244eb3b21820781b7d596e
|
|
122
|
+
recipient: 004e02a959c04cecaf111827f91caa36
|
|
123
|
+
text: Hello!
|
|
124
|
+
"""
|
|
125
|
+
And the `messages.create` is called with:
|
|
126
|
+
"""yaml
|
|
127
|
+
input:
|
|
128
|
+
sender: 004e02a959c04cecaf111827f91caa36
|
|
129
|
+
recipient: 96db5a47a8244eb3b21820781b7d596e
|
|
130
|
+
text: Hi!
|
|
131
|
+
"""
|
|
132
|
+
And the consumer `004e02a959c04cecaf111827f91caa36` is reconnected
|
|
133
|
+
Then an event is received from the stream `004e02a959c04cecaf111827f91caa36`:
|
|
134
|
+
"""yaml
|
|
135
|
+
event: default.messages.created
|
|
136
|
+
data:
|
|
137
|
+
sender: 96db5a47a8244eb3b21820781b7d596e
|
|
138
|
+
recipient: 004e02a959c04cecaf111827f91caa36
|
|
139
|
+
text: Hello!
|
|
140
|
+
"""
|
|
141
|
+
And an event is received from the stream `004e02a959c04cecaf111827f91caa36`:
|
|
142
|
+
"""yaml
|
|
143
|
+
event: default.messages.created
|
|
144
|
+
data:
|
|
145
|
+
sender: 004e02a959c04cecaf111827f91caa36
|
|
146
|
+
recipient: 96db5a47a8244eb3b21820781b7d596e
|
|
147
|
+
text: Hi!
|
|
148
|
+
"""
|
|
149
|
+
And an event is received from the stream `004e02a959c04cecaf111827f91caa36`:
|
|
150
|
+
"""yaml
|
|
151
|
+
event: token
|
|
152
|
+
"""
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import { readdirSync } from 'node:fs'
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import tsflow from 'cucumber-tsflow'
|
|
4
|
+
|
|
5
|
+
import { load as parse } from 'js-yaml'
|
|
5
6
|
import * as stage from '@toa.io/userland/stage'
|
|
6
7
|
import { type Component, type Request } from '@toa.io/core'
|
|
7
8
|
import { timeout } from '@toa.io/generic'
|
|
8
|
-
import {
|
|
9
|
+
import { parse as parseRoutes, type Declaration } from '../../source/extension.js'
|
|
10
|
+
import { Realtime } from './Realtime.js'
|
|
11
|
+
|
|
12
|
+
const { after, before, binding, given, when } = tsflow
|
|
9
13
|
|
|
10
14
|
@binding([Realtime])
|
|
11
15
|
export class Components {
|
|
@@ -18,20 +22,20 @@ export class Components {
|
|
|
18
22
|
|
|
19
23
|
@given('the `{word}` component is running with routes:')
|
|
20
24
|
public async run (component: string, yaml: string): Promise<void> {
|
|
21
|
-
const
|
|
25
|
+
const declaration = parse(yaml) as Declaration
|
|
22
26
|
const [name, namespace = 'default'] = component.split('.').reverse()
|
|
27
|
+
const routes = parseRoutes(declaration)
|
|
23
28
|
|
|
24
|
-
for (const
|
|
25
|
-
const label = `${namespace}.${name}.${event}`
|
|
26
|
-
const properties = Array.isArray(value) ? value : [value]
|
|
29
|
+
for (const route of routes) {
|
|
30
|
+
const label = `${namespace}.${name}.${route.event}`
|
|
27
31
|
|
|
28
|
-
this.realtime.declare(label, properties)
|
|
32
|
+
this.realtime.declare(label, route.properties, route.expose)
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
@when('the `{word}` is called with:')
|
|
33
37
|
public async call (endpoint: string, yaml: string): Promise<void> {
|
|
34
|
-
const request = parse
|
|
38
|
+
const request = parse(yaml) as Request
|
|
35
39
|
const [operation, component, namespace = 'default'] = endpoint.split('.').reverse()
|
|
36
40
|
const id = `${namespace}.${component}`
|
|
37
41
|
|
|
@@ -64,8 +68,4 @@ function componentPaths (): string[] {
|
|
|
64
68
|
.map((entry) => resolve(ROOT, entry.name))
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
const ROOT = resolve(
|
|
68
|
-
|
|
69
|
-
interface Manifest {
|
|
70
|
-
realtime: Map<string, string | string[]>
|
|
71
|
-
}
|
|
71
|
+
const ROOT = resolve(import.meta.dirname, 'components')
|
|
@@ -1,43 +1,39 @@
|
|
|
1
1
|
import * as boot from '@toa.io/boot'
|
|
2
|
-
import { encode } from '@toa.io/generic'
|
|
3
2
|
import { type Connector } from '@toa.io/core'
|
|
4
|
-
import
|
|
5
|
-
|
|
3
|
+
import tsflow from 'cucumber-tsflow'
|
|
4
|
+
|
|
5
|
+
import { Factory } from '../../source/index.js'
|
|
6
|
+
import type { Route } from '../../source/extension.js'
|
|
7
|
+
|
|
8
|
+
const { after, binding } = tsflow
|
|
6
9
|
|
|
7
10
|
@binding()
|
|
8
11
|
export class Realtime {
|
|
9
12
|
private readonly routes: Route[] = []
|
|
10
|
-
|
|
13
|
+
// the factory loads its binding, so the service is made when it is first served
|
|
14
|
+
private service: Connector | undefined
|
|
11
15
|
private connected = false
|
|
12
16
|
|
|
13
|
-
public constructor () {
|
|
14
|
-
this.service = new Factory(boot).service()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
17
|
@after()
|
|
18
|
-
|
|
18
|
+
public async shutdown (): Promise<void> {
|
|
19
19
|
this.connected = false
|
|
20
20
|
|
|
21
|
-
await this.service
|
|
21
|
+
await this.service?.disconnect()
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
public declare (event: string, properties: string[]): void {
|
|
25
|
-
this.routes.push({ event, properties })
|
|
24
|
+
public declare (event: string, properties: string[], expose?: string[]): void {
|
|
25
|
+
this.routes.push({ event, properties, expose })
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
public async serve (): Promise<void> {
|
|
29
29
|
if (this.connected)
|
|
30
30
|
return
|
|
31
31
|
|
|
32
|
-
process.env.TOA_REALTIME =
|
|
32
|
+
process.env.TOA_REALTIME = JSON.stringify(this.routes)
|
|
33
33
|
|
|
34
34
|
this.connected = true
|
|
35
|
+
this.service = await new Factory(boot.host()).service()
|
|
35
36
|
|
|
36
37
|
await this.service.connect()
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
interface Route {
|
|
41
|
-
event: string
|
|
42
|
-
properties: string[]
|
|
43
|
-
}
|
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import * as assert from 'node:assert'
|
|
2
|
+
import { setTimeout } from 'node:timers/promises'
|
|
3
|
+
import tsflow from 'cucumber-tsflow'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { parse } from '
|
|
5
|
+
import { match } from '@toa.io/generic'
|
|
6
|
+
import { load as parse } from 'js-yaml'
|
|
7
7
|
import * as stage from '@toa.io/userland/stage'
|
|
8
|
-
import { Realtime } from './Realtime'
|
|
8
|
+
import { Realtime } from './Realtime.js'
|
|
9
|
+
import type { Readable } from 'node:stream'
|
|
10
|
+
import type { Component } from '@toa.io/core'
|
|
11
|
+
|
|
12
|
+
const { after, binding, given, then } = tsflow
|
|
9
13
|
|
|
10
14
|
@binding([Realtime])
|
|
11
15
|
export class Streams {
|
|
12
16
|
private readonly realtime: Realtime
|
|
13
17
|
private remote: Component | null = null
|
|
14
18
|
private streams: Record<string, Readable> = {}
|
|
15
|
-
private events: Record<string,
|
|
19
|
+
private events: Record<string, Event[]> = {}
|
|
16
20
|
|
|
17
21
|
public constructor (realtime: Realtime) {
|
|
18
22
|
this.realtime = realtime
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
@given('the stream `{word}` is consumed')
|
|
25
|
+
@given('the stream `{word}` is consumed', { timeout: 30_000 })
|
|
22
26
|
public async consume (key: string): Promise<void> {
|
|
23
27
|
await this.realtime.serve()
|
|
24
28
|
|
|
25
29
|
this.remote ??= await stage.remote('realtime.streams')
|
|
26
30
|
this.events[key] = []
|
|
27
|
-
|
|
28
|
-
this.
|
|
31
|
+
|
|
32
|
+
await this.createStream(key)
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
@then('an event is received from the stream `{word}`:')
|
|
32
|
-
public received (key: string, yaml: string): void {
|
|
33
|
-
|
|
36
|
+
public async received (key: string, yaml: string): Promise<void> {
|
|
37
|
+
await setTimeout(100)
|
|
38
|
+
|
|
39
|
+
const expected = parse(yaml) as object
|
|
34
40
|
|
|
35
41
|
for (const event of this.events[key])
|
|
36
42
|
if (match(event, expected))
|
|
@@ -39,12 +45,44 @@ export class Streams {
|
|
|
39
45
|
throw new Error('No matching event received')
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
@then('the consumer `{word}` is disconnected')
|
|
49
|
+
public disconnected (key: string): void {
|
|
50
|
+
this.streams[key]?.destroy()
|
|
51
|
+
delete this.streams[key]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@then('the consumer `{word}` is reconnected')
|
|
55
|
+
public async reconnected (key: string): Promise<void> {
|
|
56
|
+
const last = this.events[key].findLast((event) => event.event === 'token')
|
|
57
|
+
|
|
58
|
+
assert.ok(last, `No last event found for stream ${key}`)
|
|
59
|
+
|
|
60
|
+
await this.createStream(key, last.data as string)
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
@after()
|
|
43
|
-
|
|
64
|
+
public async shutdown (): Promise<void> {
|
|
44
65
|
for (const stream of Object.values(this.streams))
|
|
45
66
|
stream.destroy()
|
|
46
67
|
|
|
47
68
|
this.streams = {}
|
|
48
69
|
this.events = {}
|
|
70
|
+
|
|
71
|
+
await setTimeout(100)
|
|
49
72
|
}
|
|
73
|
+
|
|
74
|
+
private async createStream (key: string, token?: string): Promise<void> {
|
|
75
|
+
this.streams[key] = await this.remote!.invoke('create', { input: { key, token } })
|
|
76
|
+
this.streams[key].on('data', (event: Event) => {
|
|
77
|
+
console.log('[TEST] Received event', event)
|
|
78
|
+
this.events[key]?.push(event)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface Event {
|
|
84
|
+
key: string
|
|
85
|
+
token: string
|
|
86
|
+
event: string
|
|
87
|
+
data?: unknown
|
|
50
88
|
}
|
|
@@ -2,15 +2,46 @@ name: messages
|
|
|
2
2
|
|
|
3
3
|
entity:
|
|
4
4
|
schema:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
properties:
|
|
6
|
+
watchers:
|
|
7
|
+
type: array
|
|
8
|
+
items:
|
|
9
|
+
type: string
|
|
10
|
+
maxLength: 32
|
|
11
|
+
sender:
|
|
12
|
+
type: string
|
|
13
|
+
maxLength: 32
|
|
14
|
+
recipient:
|
|
15
|
+
type: string
|
|
16
|
+
maxLength: 32
|
|
17
|
+
text:
|
|
18
|
+
type: string
|
|
19
|
+
type: object
|
|
20
|
+
required:
|
|
21
|
+
- sender
|
|
22
|
+
- recipient
|
|
23
|
+
- text
|
|
8
24
|
|
|
9
25
|
operations:
|
|
10
26
|
create:
|
|
11
27
|
query: false
|
|
12
28
|
forward: transit
|
|
13
29
|
input:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
properties:
|
|
31
|
+
watchers:
|
|
32
|
+
type: string
|
|
33
|
+
default: .
|
|
34
|
+
sender:
|
|
35
|
+
type: string
|
|
36
|
+
default: .
|
|
37
|
+
recipient:
|
|
38
|
+
type: string
|
|
39
|
+
default: .
|
|
40
|
+
text:
|
|
41
|
+
type: string
|
|
42
|
+
default: .
|
|
43
|
+
type: object
|
|
44
|
+
required:
|
|
45
|
+
- sender
|
|
46
|
+
- recipient
|
|
47
|
+
- text
|
package/features/steps/config.ts
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
+
import { setDefaultTimeout } from '@cucumber/cucumber'
|
|
2
|
+
|
|
1
3
|
process.env.TOA_DEV = '1'
|
|
4
|
+
|
|
5
|
+
// a reply is checked against what the operation declares, so the suite runs Toa under the
|
|
6
|
+
// contract it asks applications to keep
|
|
7
|
+
process.env.TOA_ENV ??= 'local'
|
|
8
|
+
|
|
9
|
+
// the streams component boots inside the service, and without a variable it would wait
|
|
10
|
+
// for the values service, which these features do not run
|
|
11
|
+
process.env.TOA_CONFIGURATION_REALTIME_STREAMS ??= '{}'
|
|
12
|
+
|
|
13
|
+
setDefaultTimeout(60 * 1000)
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.realtime",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.283",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Toa Realtime",
|
|
5
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
7
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -16,13 +17,13 @@
|
|
|
16
17
|
"publishConfig": {
|
|
17
18
|
"access": "public"
|
|
18
19
|
},
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@toa.io/core": "1.0.0-alpha.283",
|
|
22
|
+
"openspan": "1.0.0-alpha.277"
|
|
22
23
|
},
|
|
23
24
|
"scripts": {
|
|
24
|
-
"transpile": "npx tsc && npx tsc -p ./components/streams",
|
|
25
|
-
"features": "
|
|
25
|
+
"transpile": "npx tsc && npx tsc -p ./components/realtime.streams",
|
|
26
|
+
"features": "TSX_TSCONFIG_PATH=features/steps/tsconfig.json NODE_OPTIONS=--import=tsx cucumber-js"
|
|
26
27
|
},
|
|
27
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "2f52b023bed0ccb2ecdb2b82fe59fbe38bf282e1"
|
|
28
29
|
}
|
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](
|
|
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:
|
|
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`,
|
|
@@ -87,13 +101,25 @@ Dynamic routes are managed by the `realtime.routes` component, running in the Re
|
|
|
87
101
|
|
|
88
102
|
## Exposition
|
|
89
103
|
|
|
90
|
-
Streams are exposed by the [`realtime.streams`](components/streams) component, running in the
|
|
104
|
+
Streams are exposed by the [`realtime.streams`](components/realtime.streams) component, running in the
|
|
91
105
|
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
|
-
- [
|
|
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
|
+
```
|
package/source/Composition.ts
CHANGED
|
@@ -1,33 +1,27 @@
|
|
|
1
1
|
import { readdirSync, type Dirent } from 'node:fs'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import { Connector } from '@toa.io/core'
|
|
4
|
-
import { type
|
|
4
|
+
import { type Host } from './Factory.js'
|
|
5
5
|
|
|
6
6
|
export class Composition extends Connector {
|
|
7
|
-
private readonly
|
|
7
|
+
private readonly host: Host
|
|
8
8
|
|
|
9
|
-
public constructor (
|
|
9
|
+
public constructor (host: Host) {
|
|
10
10
|
super()
|
|
11
|
-
this.
|
|
11
|
+
this.host = host
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
protected override async open (): Promise<void> {
|
|
15
15
|
const paths = find()
|
|
16
|
-
const composition = await this.
|
|
16
|
+
const composition = await this.host.composition(paths)
|
|
17
17
|
|
|
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
|
|
|
@@ -37,4 +31,21 @@ function entries (): Dirent[] {
|
|
|
37
31
|
return entries.filter((entry) => entry.isDirectory())
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
|
|
34
|
+
export function components (): Components {
|
|
35
|
+
const labels: string[] = []
|
|
36
|
+
const paths: string[] = []
|
|
37
|
+
|
|
38
|
+
for (const entry of entries()) {
|
|
39
|
+
labels.push(entry.name.replace('.', '-'))
|
|
40
|
+
paths.push(resolve(ROOT, entry.name))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { labels, paths }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Components {
|
|
47
|
+
labels: string[]
|
|
48
|
+
paths: string[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ROOT = resolve(import.meta.dirname, '../components/')
|
package/source/Factory.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { type Component, type extensions, Locator } from '@toa.io/core'
|
|
2
|
-
import { Realtime } from './Realtime'
|
|
3
|
-
import { Composition } from './Composition'
|
|
4
|
-
import { Routes } from './Routes'
|
|
2
|
+
import { Realtime } from './Realtime.js'
|
|
3
|
+
import { Composition } from './Composition.js'
|
|
4
|
+
import { Routes } from './Routes.js'
|
|
5
5
|
|
|
6
6
|
export class Factory implements extensions.Factory {
|
|
7
|
-
private readonly
|
|
7
|
+
private readonly host: Host
|
|
8
8
|
|
|
9
|
-
public constructor (
|
|
10
|
-
this.
|
|
9
|
+
public constructor (host: Host) {
|
|
10
|
+
this.host = host
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
public service (): Realtime {
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const realtime = new Realtime(routes, discovery)
|
|
14
|
+
const routes = new Routes(this.host)
|
|
15
|
+
const composition = new Composition(this.host)
|
|
16
|
+
const realtime = new Realtime(routes, async () => await this.discovery())
|
|
18
17
|
|
|
19
18
|
realtime.depends(routes)
|
|
20
19
|
realtime.depends(composition)
|
|
@@ -25,9 +24,8 @@ export class Factory implements extensions.Factory {
|
|
|
25
24
|
private async discovery (): Promise<Component> {
|
|
26
25
|
const locator = new Locator('streams', 'realtime')
|
|
27
26
|
|
|
28
|
-
return await this.
|
|
27
|
+
return await this.host.remote(locator, { service: 'realtime' })
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
export type Bootloader = typeof import('@toa.io/boot')
|
|
31
|
+
export type Host = extensions.Host
|