@toa.io/extensions.realtime 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.
- package/package.json +14 -5
- package/transpiled/Routes.d.ts +1 -1
- package/transpiled/index.d.ts +1 -1
- package/transpiled/index.js +1 -1
- package/transpiled/index.js.map +1 -1
- package/.readme/overview-dark.jpg +0 -0
- package/.readme/overview-light.jpg +0 -0
- package/CHANGELOG.md +0 -35
- package/components/realtime.streams/tsconfig.tsbuildinfo +0 -1
- package/context.toa.yaml +0 -10
- package/cucumber.mjs +0 -8
- package/features/static.feature +0 -152
- package/features/steps/Components.ts +0 -72
- package/features/steps/Realtime.ts +0 -40
- package/features/steps/Streams.ts +0 -85
- package/features/steps/components/messages/manifest.toa.yaml +0 -45
- package/features/steps/config.ts +0 -15
- package/features/steps/tsconfig.json +0 -9
- package/source/Composition.ts +0 -51
- package/source/Factory.ts +0 -32
- package/source/Realtime.ts +0 -71
- package/source/Receiver.ts +0 -95
- package/source/Routes.ts +0 -65
- package/source/extension.ts +0 -92
- package/source/index.ts +0 -2
- package/transpiled/extension.d.ts +0 -22
- package/transpiled/extension.js +0 -55
- package/transpiled/extension.js.map +0 -1
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import { readdirSync } from 'node:fs'
|
|
3
|
-
import tsflow from 'cucumber-tsflow'
|
|
4
|
-
|
|
5
|
-
import { load as parse } from 'js-yaml'
|
|
6
|
-
import * as stage from '@toa.io/userland/stage'
|
|
7
|
-
import { type Component } from '@toa.io/core'
|
|
8
|
-
import type { Request } from '@toa.io/core/types'
|
|
9
|
-
import { timeout } from '@toa.io/generic'
|
|
10
|
-
import { parse as parseRoutes, type Declaration } from '../../source/extension.js'
|
|
11
|
-
import { Realtime } from './Realtime.js'
|
|
12
|
-
|
|
13
|
-
const { after, before, binding, given, when } = tsflow
|
|
14
|
-
|
|
15
|
-
@binding([Realtime])
|
|
16
|
-
export class Components {
|
|
17
|
-
private readonly realtime: Realtime
|
|
18
|
-
private remotes: Record<string, Component> = {}
|
|
19
|
-
|
|
20
|
-
public constructor(realtime: Realtime) {
|
|
21
|
-
this.realtime = realtime
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
@given('the `{word}` component is running with routes:')
|
|
25
|
-
public async run(component: string, yaml: string): Promise<void> {
|
|
26
|
-
const declaration = parse(yaml) as Declaration
|
|
27
|
-
const [name, namespace = 'default'] = component.split('.').reverse()
|
|
28
|
-
const routes = parseRoutes(declaration)
|
|
29
|
-
|
|
30
|
-
for (const route of routes) {
|
|
31
|
-
const label = `${namespace}.${name}.${route.event}`
|
|
32
|
-
|
|
33
|
-
this.realtime.declare(label, route.properties, route.expose)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
@when('the `{word}` is called with:')
|
|
38
|
-
public async call(endpoint: string, yaml: string): Promise<void> {
|
|
39
|
-
const request = parse(yaml) as Request
|
|
40
|
-
const [operation, component, namespace = 'default'] = endpoint.split('.').reverse()
|
|
41
|
-
const id = `${namespace}.${component}`
|
|
42
|
-
|
|
43
|
-
this.remotes[id] ??= await stage.remote(id)
|
|
44
|
-
|
|
45
|
-
await this.remotes[id].invoke(operation, request)
|
|
46
|
-
await timeout(500)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
@before()
|
|
50
|
-
private async compose(): Promise<void> {
|
|
51
|
-
const paths = componentPaths()
|
|
52
|
-
|
|
53
|
-
await stage.compose(paths)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@after()
|
|
57
|
-
private async shutdown(): Promise<void> {
|
|
58
|
-
this.remotes = {}
|
|
59
|
-
|
|
60
|
-
await stage.shutdown()
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function componentPaths(): string[] {
|
|
65
|
-
const entries = readdirSync(ROOT, { withFileTypes: true })
|
|
66
|
-
|
|
67
|
-
return entries
|
|
68
|
-
.filter((entry) => entry.isDirectory())
|
|
69
|
-
.map((entry) => resolve(ROOT, entry.name))
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const ROOT = resolve(import.meta.dirname, 'components')
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import * as boot from '@toa.io/boot'
|
|
2
|
-
import { type Connector } from '@toa.io/core'
|
|
3
|
-
import { environment } from '@toa.io/generic'
|
|
4
|
-
|
|
5
|
-
import tsflow from 'cucumber-tsflow'
|
|
6
|
-
|
|
7
|
-
import { Factory } from '../../source/index.js'
|
|
8
|
-
import type { Route } from '../../source/extension.js'
|
|
9
|
-
|
|
10
|
-
const { after, binding } = tsflow
|
|
11
|
-
|
|
12
|
-
@binding()
|
|
13
|
-
export class Realtime {
|
|
14
|
-
private readonly routes: Route[] = []
|
|
15
|
-
// the factory loads its binding, so the service is made when it is first served
|
|
16
|
-
private service: Connector | undefined
|
|
17
|
-
private connected = false
|
|
18
|
-
|
|
19
|
-
@after()
|
|
20
|
-
public async shutdown(): Promise<void> {
|
|
21
|
-
this.connected = false
|
|
22
|
-
|
|
23
|
-
await this.service?.disconnect()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
public declare(event: string, properties: string[], expose?: string[]): void {
|
|
27
|
-
this.routes.push({ event, properties, expose })
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
public async serve(): Promise<void> {
|
|
31
|
-
if (this.connected) return
|
|
32
|
-
|
|
33
|
-
environment.set('TOA_REALTIME', JSON.stringify(this.routes))
|
|
34
|
-
|
|
35
|
-
this.connected = true
|
|
36
|
-
this.service = await new Factory(boot.host()).service()
|
|
37
|
-
|
|
38
|
-
await this.service.connect()
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import * as assert from 'node:assert'
|
|
2
|
-
import { setTimeout } from 'node:timers/promises'
|
|
3
|
-
import tsflow from 'cucumber-tsflow'
|
|
4
|
-
|
|
5
|
-
import { match } from '@toa.io/generic'
|
|
6
|
-
import { load as parse } from 'js-yaml'
|
|
7
|
-
import * as stage from '@toa.io/userland/stage'
|
|
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
|
|
13
|
-
|
|
14
|
-
@binding([Realtime])
|
|
15
|
-
export class Streams {
|
|
16
|
-
private readonly realtime: Realtime
|
|
17
|
-
private remote: Component | null = null
|
|
18
|
-
private streams: Record<string, Readable> = {}
|
|
19
|
-
private events: Record<string, Event[]> = {}
|
|
20
|
-
|
|
21
|
-
public constructor(realtime: Realtime) {
|
|
22
|
-
this.realtime = realtime
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@given('the stream `{word}` is consumed', { timeout: 30_000 })
|
|
26
|
-
public async consume(key: string): Promise<void> {
|
|
27
|
-
await this.realtime.serve()
|
|
28
|
-
|
|
29
|
-
this.remote ??= await stage.remote('realtime.streams')
|
|
30
|
-
this.events[key] = []
|
|
31
|
-
|
|
32
|
-
await this.createStream(key)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@then('an event is received from the stream `{word}`:')
|
|
36
|
-
public async received(key: string, yaml: string): Promise<void> {
|
|
37
|
-
await setTimeout(100)
|
|
38
|
-
|
|
39
|
-
const expected = parse(yaml) as object
|
|
40
|
-
|
|
41
|
-
for (const event of this.events[key]) if (match(event, expected)) return
|
|
42
|
-
|
|
43
|
-
throw new Error('No matching event received')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
@then('the consumer `{word}` is disconnected')
|
|
47
|
-
public disconnected(key: string): void {
|
|
48
|
-
this.streams[key]?.destroy()
|
|
49
|
-
delete this.streams[key]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
@then('the consumer `{word}` is reconnected')
|
|
53
|
-
public async reconnected(key: string): Promise<void> {
|
|
54
|
-
const last = this.events[key].findLast((event) => event.event === 'token')
|
|
55
|
-
|
|
56
|
-
assert.ok(last, `No last event found for stream ${key}`)
|
|
57
|
-
|
|
58
|
-
await this.createStream(key, last.data as string)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
@after()
|
|
62
|
-
public async shutdown(): Promise<void> {
|
|
63
|
-
for (const stream of Object.values(this.streams)) stream.destroy()
|
|
64
|
-
|
|
65
|
-
this.streams = {}
|
|
66
|
-
this.events = {}
|
|
67
|
-
|
|
68
|
-
await setTimeout(100)
|
|
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
|
|
85
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
name: messages
|
|
2
|
-
|
|
3
|
-
entity:
|
|
4
|
-
properties:
|
|
5
|
-
watchers:
|
|
6
|
-
type: array
|
|
7
|
-
items:
|
|
8
|
-
type: string
|
|
9
|
-
maxLength: 32
|
|
10
|
-
sender:
|
|
11
|
-
type: string
|
|
12
|
-
maxLength: 32
|
|
13
|
-
recipient:
|
|
14
|
-
type: string
|
|
15
|
-
maxLength: 32
|
|
16
|
-
text:
|
|
17
|
-
type: string
|
|
18
|
-
required:
|
|
19
|
-
- sender
|
|
20
|
-
- recipient
|
|
21
|
-
- text
|
|
22
|
-
|
|
23
|
-
operations:
|
|
24
|
-
create:
|
|
25
|
-
query: false
|
|
26
|
-
forward: transit
|
|
27
|
-
input:
|
|
28
|
-
properties:
|
|
29
|
-
watchers:
|
|
30
|
-
type: string
|
|
31
|
-
default: .
|
|
32
|
-
sender:
|
|
33
|
-
type: string
|
|
34
|
-
default: .
|
|
35
|
-
recipient:
|
|
36
|
-
type: string
|
|
37
|
-
default: .
|
|
38
|
-
text:
|
|
39
|
-
type: string
|
|
40
|
-
default: .
|
|
41
|
-
type: object
|
|
42
|
-
required:
|
|
43
|
-
- sender
|
|
44
|
-
- recipient
|
|
45
|
-
- text
|
package/features/steps/config.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { setDefaultTimeout } from '@cucumber/cucumber'
|
|
2
|
-
import { environment } from '@toa.io/generic'
|
|
3
|
-
|
|
4
|
-
environment.set('TOA_DEV', '1')
|
|
5
|
-
|
|
6
|
-
// a reply is checked against what the operation declares, so the suite runs Toa under the
|
|
7
|
-
// contract it asks applications to keep
|
|
8
|
-
if (!environment.has('TOA_ENV')) environment.set('TOA_ENV', 'local')
|
|
9
|
-
|
|
10
|
-
// the streams component boots inside the service, and without a variable it would wait
|
|
11
|
-
// for the values service, which these features do not run
|
|
12
|
-
if (!environment.has('TOA_CONFIGURATION_REALTIME_STREAMS'))
|
|
13
|
-
environment.set('TOA_CONFIGURATION_REALTIME_STREAMS', '{}')
|
|
14
|
-
|
|
15
|
-
setDefaultTimeout(60 * 1000)
|
package/source/Composition.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { readdirSync, type Dirent } from 'node:fs'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import { Connector } from '@toa.io/core'
|
|
4
|
-
import { type Host } from './Factory.js'
|
|
5
|
-
|
|
6
|
-
export class Composition extends Connector {
|
|
7
|
-
private readonly host: Host
|
|
8
|
-
|
|
9
|
-
public constructor(host: Host) {
|
|
10
|
-
super()
|
|
11
|
-
this.host = host
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
protected override async open(): Promise<void> {
|
|
15
|
-
const paths = find()
|
|
16
|
-
const composition = await this.host.composition(paths)
|
|
17
|
-
|
|
18
|
-
await composition.connect()
|
|
19
|
-
|
|
20
|
-
this.depends(composition)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function find(): string[] {
|
|
25
|
-
return entries().map((entry) => resolve(ROOT, entry.name))
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function entries(): Dirent[] {
|
|
29
|
-
const entries = readdirSync(ROOT, { withFileTypes: true })
|
|
30
|
-
|
|
31
|
-
return entries.filter((entry) => entry.isDirectory())
|
|
32
|
-
}
|
|
33
|
-
|
|
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
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { type Component, Locator } from '@toa.io/core'
|
|
2
|
-
import type { extensions } from '@toa.io/core/types'
|
|
3
|
-
import { Realtime } from './Realtime.js'
|
|
4
|
-
import { Composition } from './Composition.js'
|
|
5
|
-
import { Routes } from './Routes.js'
|
|
6
|
-
|
|
7
|
-
export class Factory implements extensions.Factory {
|
|
8
|
-
private readonly host: Host
|
|
9
|
-
|
|
10
|
-
public constructor(host: Host) {
|
|
11
|
-
this.host = host
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
public service(): Realtime {
|
|
15
|
-
const routes = new Routes(this.host)
|
|
16
|
-
const composition = new Composition(this.host)
|
|
17
|
-
const realtime = new Realtime(routes, async () => await this.discovery())
|
|
18
|
-
|
|
19
|
-
realtime.depends(routes)
|
|
20
|
-
realtime.depends(composition)
|
|
21
|
-
|
|
22
|
-
return realtime
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
private async discovery(): Promise<Component> {
|
|
26
|
-
const locator = new Locator('streams', 'realtime')
|
|
27
|
-
|
|
28
|
-
return await this.host.remote(locator, { service: 'realtime' })
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type Host = extensions.Host
|
package/source/Realtime.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { console, run } from 'openspan'
|
|
2
|
-
import { type Component, Connector } from '@toa.io/core'
|
|
3
|
-
import { type Routes } from './Routes.js'
|
|
4
|
-
import type { Push } from './Receiver.js'
|
|
5
|
-
|
|
6
|
-
export class Realtime extends Connector {
|
|
7
|
-
private readonly discovery: () => Promise<Component>
|
|
8
|
-
private streams: Component | null = null
|
|
9
|
-
|
|
10
|
-
public constructor(routes: Routes, discovery: () => Promise<Component>) {
|
|
11
|
-
super()
|
|
12
|
-
|
|
13
|
-
this.discovery = discovery
|
|
14
|
-
|
|
15
|
-
routes.events.on('data', this.push.bind(this))
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
protected override async open(): Promise<void> {
|
|
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()
|
|
23
|
-
this.depends(this.streams)
|
|
24
|
-
|
|
25
|
-
await this.streams.connect()
|
|
26
|
-
|
|
27
|
-
console.info('Realtime service started')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
protected override dispose(): void {
|
|
31
|
-
console.info('Realtime service shutdown complete')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private push({ telemetry, ...event }: Push): void {
|
|
35
|
-
const processing =
|
|
36
|
-
telemetry === null
|
|
37
|
-
? this.deliver(event)
|
|
38
|
-
: run(telemetry, async () => await this.deliver(event))
|
|
39
|
-
|
|
40
|
-
void processing.catch((error) => console.error('Realtime push failed', error))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private async deliver(event: Omit<Push, 'telemetry'>): Promise<void> {
|
|
44
|
-
/*
|
|
45
|
-
* The delivery span is created on behalf of the messaging destination
|
|
46
|
-
* (same as the core Receiver), so that service graphs display the fan-out:
|
|
47
|
-
* producer -> destination -> realtime
|
|
48
|
-
*/
|
|
49
|
-
const delivery = {
|
|
50
|
-
name: `${event.event} deliver`,
|
|
51
|
-
kind: 'producer' as const,
|
|
52
|
-
service: event.event,
|
|
53
|
-
attributes: { 'messaging.destination.name': event.event }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const options = {
|
|
57
|
-
name: `${event.event} push`,
|
|
58
|
-
kind: 'consumer' as const,
|
|
59
|
-
service: 'realtime',
|
|
60
|
-
attributes: { 'messaging.destination.name': event.event }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
await console.span(
|
|
64
|
-
delivery,
|
|
65
|
-
async () =>
|
|
66
|
-
await console.span(options, async () => {
|
|
67
|
-
await this.streams?.invoke('push', { input: event })
|
|
68
|
-
})
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
}
|
package/source/Receiver.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { console, decode, run } from 'openspan'
|
|
2
|
-
import { Connector } from '@toa.io/core'
|
|
3
|
-
import type { Message } from '@toa.io/core/types'
|
|
4
|
-
import type { SpanContext } from 'openspan'
|
|
5
|
-
import type { Readable } from 'node:stream'
|
|
6
|
-
|
|
7
|
-
export class Receiver extends Connector {
|
|
8
|
-
private readonly event: string
|
|
9
|
-
private readonly properties: string[]
|
|
10
|
-
private readonly expose?: string[]
|
|
11
|
-
private readonly stream: Readable
|
|
12
|
-
|
|
13
|
-
public constructor({
|
|
14
|
-
event,
|
|
15
|
-
properties,
|
|
16
|
-
stream,
|
|
17
|
-
expose
|
|
18
|
-
}: {
|
|
19
|
-
event: string
|
|
20
|
-
properties: string[]
|
|
21
|
-
stream: Readable
|
|
22
|
-
expose?: string[]
|
|
23
|
-
}) {
|
|
24
|
-
super()
|
|
25
|
-
|
|
26
|
-
this.event = event
|
|
27
|
-
this.properties = properties
|
|
28
|
-
this.expose = expose
|
|
29
|
-
this.stream = stream
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
public async receive(message: Message<Record<string, string>>): Promise<void> {
|
|
33
|
-
// the push continues the trace from the producer
|
|
34
|
-
const telemetry = message.telemetry === undefined ? null : decode(message.telemetry)
|
|
35
|
-
|
|
36
|
-
if (telemetry === null) this.process(message, telemetry)
|
|
37
|
-
else run(telemetry, () => this.process(message, telemetry))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
private process(
|
|
41
|
-
message: Message<Record<string, string>>,
|
|
42
|
-
telemetry: SpanContext | null
|
|
43
|
-
): void {
|
|
44
|
-
const data = this.fit(message.payload)
|
|
45
|
-
|
|
46
|
-
for (const property of this.properties) {
|
|
47
|
-
const key = message.payload[property]
|
|
48
|
-
|
|
49
|
-
if (key === undefined) {
|
|
50
|
-
console.debug('Event does not contain key property', {
|
|
51
|
-
property,
|
|
52
|
-
event: this.event
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
continue
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (Array.isArray(key))
|
|
59
|
-
// eslint-disable-next-line max-depth
|
|
60
|
-
for (const k of key as string[]) this.push(k, data, telemetry)
|
|
61
|
-
else this.push(key, data, telemetry)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private fit(payload: Record<string, string>): Record<string, string> {
|
|
66
|
-
if (this.expose === undefined) return payload
|
|
67
|
-
|
|
68
|
-
const entries = Object.entries(payload).filter(([key]) => this.expose!.includes(key))
|
|
69
|
-
|
|
70
|
-
return Object.fromEntries(entries)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private push(
|
|
74
|
-
key: string | null,
|
|
75
|
-
data: Record<string, string>,
|
|
76
|
-
telemetry: SpanContext | null
|
|
77
|
-
): void {
|
|
78
|
-
if (key === null || typeof key === 'undefined') {
|
|
79
|
-
console.debug('Key is null or undefined, skipping', { key, event: this.event })
|
|
80
|
-
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
console.debug('Pushing event to stream', { key, event: this.event, data })
|
|
85
|
-
|
|
86
|
-
this.stream.push({ key, event: this.event, data, telemetry } satisfies Push)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface Push {
|
|
91
|
-
key: string
|
|
92
|
-
event: string
|
|
93
|
-
data: Record<string, string>
|
|
94
|
-
telemetry: SpanContext | null
|
|
95
|
-
}
|
package/source/Routes.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { console } from 'openspan'
|
|
3
|
-
import { Connector } from '@toa.io/core'
|
|
4
|
-
import { Receiver } from './Receiver.js'
|
|
5
|
-
import { environment } from '@toa.io/generic'
|
|
6
|
-
import type { Route } from './extension.js'
|
|
7
|
-
import type { Host } from './Factory.js'
|
|
8
|
-
|
|
9
|
-
export class Routes extends Connector {
|
|
10
|
-
public events = new Events()
|
|
11
|
-
|
|
12
|
-
private readonly host: Host
|
|
13
|
-
|
|
14
|
-
public constructor(host: Host) {
|
|
15
|
-
super()
|
|
16
|
-
|
|
17
|
-
this.host = host
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
private static read(): Route[] {
|
|
21
|
-
const value = environment.get('TOA_REALTIME')
|
|
22
|
-
|
|
23
|
-
if (value === undefined) throw new Error('TOA_REALTIME is not defined')
|
|
24
|
-
|
|
25
|
-
return JSON.parse(value) as Route[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
public override async open(): Promise<void> {
|
|
29
|
-
const routes = Routes.read()
|
|
30
|
-
const creating = []
|
|
31
|
-
|
|
32
|
-
for (const { event, properties, expose } of routes) {
|
|
33
|
-
const consumer = this.host.receive(
|
|
34
|
-
event,
|
|
35
|
-
new Receiver({ event, properties, stream: this.events, expose })
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
creating.push(consumer)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const consumers = await Promise.all(creating)
|
|
42
|
-
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
44
|
-
const connecting = consumers.map((consumer) => consumer.connect())
|
|
45
|
-
|
|
46
|
-
await Promise.all(connecting)
|
|
47
|
-
this.depends(consumers)
|
|
48
|
-
|
|
49
|
-
console.info('Event sources connected', { count: creating.length })
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public override async close(): Promise<void> {
|
|
53
|
-
console.info('Event sources disconnected')
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
class Events extends Readable {
|
|
58
|
-
public constructor() {
|
|
59
|
-
super({ objectMode: true })
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
public override _read(): void {}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export type { Route }
|
package/source/extension.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
/** Where Toa's release publishes this service's image. An application takes it
|
|
6
|
-
* instead of building one when its context says `registry.services: published`. */
|
|
7
|
-
export const image = 'ghcr.io/toa-io/extension-realtime-streams'
|
|
8
|
-
|
|
9
|
-
export const standalone = true
|
|
10
|
-
export { components } from './Composition.js'
|
|
11
|
-
|
|
12
|
-
export function deployment(
|
|
13
|
-
instances: Instances<Declaration>,
|
|
14
|
-
annotation?: Declaration & Annotation
|
|
15
|
-
): Dependency {
|
|
16
|
-
const routes = []
|
|
17
|
-
const { resources, ...annotatedRoutes } = annotation ?? {}
|
|
18
|
-
const labels = components().labels
|
|
19
|
-
|
|
20
|
-
if (annotatedRoutes !== undefined) routes.push(...parse(annotatedRoutes))
|
|
21
|
-
|
|
22
|
-
for (const instance of instances) {
|
|
23
|
-
const completed: Declaration = {}
|
|
24
|
-
|
|
25
|
-
for (const [key, value] of Object.entries(instance.manifest)) {
|
|
26
|
-
const event = instance.locator.id + '.' + key
|
|
27
|
-
|
|
28
|
-
completed[event] = value
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
routes.push(...parse(completed))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const service: Service = {
|
|
35
|
-
group: 'realtime',
|
|
36
|
-
name: 'streams',
|
|
37
|
-
image,
|
|
38
|
-
|
|
39
|
-
version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
40
|
-
.version,
|
|
41
|
-
components: labels,
|
|
42
|
-
resources,
|
|
43
|
-
variables: [
|
|
44
|
-
{
|
|
45
|
-
name: 'TOA_REALTIME',
|
|
46
|
-
value: JSON.stringify(routes)
|
|
47
|
-
}
|
|
48
|
-
]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return { services: [service], events: routes.map((route) => route.event) }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function parse(declaration: Declaration): Route[] {
|
|
55
|
-
const routes: Route[] = []
|
|
56
|
-
|
|
57
|
-
for (const [event, value] of Object.entries(declaration))
|
|
58
|
-
if (isObject(value)) {
|
|
59
|
-
const properties = Array.isArray(value.key) ? value.key : [value.key]
|
|
60
|
-
|
|
61
|
-
routes.push({ event, properties, expose: value.expose })
|
|
62
|
-
} else {
|
|
63
|
-
const properties = Array.isArray(value) ? value : [value]
|
|
64
|
-
|
|
65
|
-
routes.push({ event, properties })
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return routes
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function isObject(value: Entry): value is RouteDeclaration {
|
|
72
|
-
return typeof value === 'object' && !Array.isArray(value)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export type Entry = string | string[] | RouteDeclaration
|
|
76
|
-
|
|
77
|
-
export type Declaration = Record<string, Entry>
|
|
78
|
-
|
|
79
|
-
export interface RouteDeclaration {
|
|
80
|
-
key: string | string[]
|
|
81
|
-
expose?: string[]
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface Route {
|
|
85
|
-
event: string
|
|
86
|
-
properties: string[]
|
|
87
|
-
expose?: string[]
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
interface Annotation {
|
|
91
|
-
resources?: Resources
|
|
92
|
-
}
|
package/source/index.ts
DELETED