@wandelbots/nova-js 3.12.0 → 3.13.0-pr.307.0c31cef
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/dist/experimental/nats/index.cjs +104 -0
- package/dist/experimental/nats/index.cjs.map +1 -0
- package/dist/experimental/nats/index.d.cts +2223 -0
- package/dist/experimental/nats/index.d.cts.map +1 -0
- package/dist/experimental/nats/index.d.mts +2223 -0
- package/dist/experimental/nats/index.d.mts.map +1 -0
- package/dist/experimental/nats/index.mjs +103 -0
- package/dist/experimental/nats/index.mjs.map +1 -0
- package/package.json +11 -2
- package/src/asyncapi.yaml +3593 -0
- package/src/experimental/nats/index.ts +16 -0
- package/src/lib/experimental/nats/NovaNatsClient.ts +101 -0
- package/src/lib/experimental/nats/buildSubject.ts +68 -0
- package/src/lib/experimental/nats/generated/operations.ts +203 -0
- package/src/lib/experimental/nats/generated/types.ts +2325 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experimental typed NATS client for the Wandelbots NOVA messaging API,
|
|
3
|
+
* generated from src/asyncapi.yaml (see scripts/generate-nats-client.ts).
|
|
4
|
+
*
|
|
5
|
+
* This API is experimental and may change without a major version bump.
|
|
6
|
+
*/
|
|
7
|
+
export type {
|
|
8
|
+
NatsOperationParams,
|
|
9
|
+
NatsReplyPayloads,
|
|
10
|
+
NatsRequestPayloads,
|
|
11
|
+
NatsRequestSubject,
|
|
12
|
+
NatsSubscribePayloads,
|
|
13
|
+
NatsSubscribeSubject,
|
|
14
|
+
} from "../../lib/experimental/nats/generated/operations.ts"
|
|
15
|
+
export { NovaNatsClient } from "../../lib/experimental/nats/NovaNatsClient.ts"
|
|
16
|
+
export type { NovaNatsClientConfig } from "../../lib/experimental/nats/NovaNatsClient.ts"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConnectionOptions,
|
|
3
|
+
type Msg,
|
|
4
|
+
type NatsConnection,
|
|
5
|
+
wsconnect,
|
|
6
|
+
} from "@nats-io/nats-core"
|
|
7
|
+
import { buildSubject } from "./buildSubject.ts"
|
|
8
|
+
import type {
|
|
9
|
+
NatsOperationParams,
|
|
10
|
+
NatsReplyPayloads,
|
|
11
|
+
NatsRequestPayloads,
|
|
12
|
+
NatsRequestSubject,
|
|
13
|
+
NatsSubscribePayloads,
|
|
14
|
+
NatsSubscribeSubject,
|
|
15
|
+
} from "./generated/operations.ts"
|
|
16
|
+
|
|
17
|
+
export type NovaNatsClientConfig = ConnectionOptions
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Typed NATS client for the Wandelbots NOVA messaging API, generated from
|
|
21
|
+
* src/asyncapi.yaml (see scripts/generate-nats-client.ts).
|
|
22
|
+
*
|
|
23
|
+
* Connects over WebSocket via `@nats-io/nats-core`'s `wsconnect`.
|
|
24
|
+
*/
|
|
25
|
+
export class NovaNatsClient {
|
|
26
|
+
readonly config: NovaNatsClientConfig
|
|
27
|
+
connection: NatsConnection | null = null
|
|
28
|
+
|
|
29
|
+
constructor(config: NovaNatsClientConfig) {
|
|
30
|
+
this.config = config
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Connects to NATS if not already connected, and returns the connection. */
|
|
34
|
+
async connect(): Promise<NatsConnection> {
|
|
35
|
+
if (!this.connection) {
|
|
36
|
+
this.connection = await wsconnect(this.config)
|
|
37
|
+
}
|
|
38
|
+
return this.connection
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Closes the underlying NATS connection, if open. */
|
|
42
|
+
async close(): Promise<void> {
|
|
43
|
+
await this.connection?.close()
|
|
44
|
+
this.connection = null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Subscribes to a NATS subject published by the server, invoking `handler`
|
|
49
|
+
* with the JSON-decoded payload of every message received.
|
|
50
|
+
*
|
|
51
|
+
* `subject` is the subject template as it appears on the wire, e.g.
|
|
52
|
+
* `"nova.v2.cells.{cell}"`, with `{param}` placeholders filled in from
|
|
53
|
+
* `params`.
|
|
54
|
+
*
|
|
55
|
+
* Returns a function that unsubscribes when called.
|
|
56
|
+
*/
|
|
57
|
+
async subscribe<K extends NatsSubscribeSubject>(
|
|
58
|
+
subject: K,
|
|
59
|
+
params: NatsOperationParams[K],
|
|
60
|
+
handler: (payload: NatsSubscribePayloads[K], msg: Msg) => void,
|
|
61
|
+
): Promise<() => void> {
|
|
62
|
+
const nc = await this.connect()
|
|
63
|
+
const resolvedSubject = buildSubject(subject, params)
|
|
64
|
+
const sub = nc.subscribe(resolvedSubject)
|
|
65
|
+
|
|
66
|
+
;(async () => {
|
|
67
|
+
for await (const msg of sub) {
|
|
68
|
+
handler(msg.json<NatsSubscribePayloads[K]>(), msg)
|
|
69
|
+
}
|
|
70
|
+
})().catch((err: unknown) => {
|
|
71
|
+
console.error(
|
|
72
|
+
`Error handling NATS subscription for "${resolvedSubject}"`,
|
|
73
|
+
err,
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return () => sub.unsubscribe()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Sends a request payload for a NATS subject the server receives, and
|
|
82
|
+
* waits for the JSON-decoded reply.
|
|
83
|
+
*
|
|
84
|
+
* `subject` is the subject template as it appears on the wire, e.g.
|
|
85
|
+
* `"nova.v2.cells.{cell}.bus-ios.ios.set"`, with `{param}` placeholders
|
|
86
|
+
* filled in from `params`.
|
|
87
|
+
*/
|
|
88
|
+
async request<K extends NatsRequestSubject>(
|
|
89
|
+
subject: K,
|
|
90
|
+
params: NatsOperationParams[K],
|
|
91
|
+
payload: NatsRequestPayloads[K],
|
|
92
|
+
opts: { timeout?: number } = {},
|
|
93
|
+
): Promise<NatsReplyPayloads[K]> {
|
|
94
|
+
const nc = await this.connect()
|
|
95
|
+
const resolvedSubject = buildSubject(subject, params)
|
|
96
|
+
const msg = await nc.request(resolvedSubject, JSON.stringify(payload), {
|
|
97
|
+
timeout: opts.timeout ?? 5000,
|
|
98
|
+
})
|
|
99
|
+
return msg.json<NatsReplyPayloads[K]>()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a NATS subject from an AsyncAPI-style channel address template
|
|
3
|
+
* (e.g. `"{instance}.v2.cells.{cell}"`) by substituting each `{param}`
|
|
4
|
+
* placeholder with the corresponding value from `params`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function isValidSubjectChar(char: string): boolean {
|
|
8
|
+
const code = char.charCodeAt(0)
|
|
9
|
+
return (
|
|
10
|
+
(code >= 48 && code <= 57) || // 0-9
|
|
11
|
+
(code >= 65 && code <= 90) || // A-Z
|
|
12
|
+
(code >= 97 && code <= 122) || // a-z
|
|
13
|
+
char === "-" ||
|
|
14
|
+
char === "_"
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isValidSubjectValue(value: string): boolean {
|
|
19
|
+
if (value.length === 0) return false
|
|
20
|
+
for (const char of value) {
|
|
21
|
+
if (!isValidSubjectChar(char)) return false
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildSubject(
|
|
27
|
+
template: string,
|
|
28
|
+
params: Record<string, string>,
|
|
29
|
+
): string {
|
|
30
|
+
// Scanned manually (rather than with a regex like /\{([^}]+)\}/g) to avoid
|
|
31
|
+
// a polynomial-time backtracking blowup on pathological input, e.g. a
|
|
32
|
+
// template consisting of many "{" characters with no closing "}".
|
|
33
|
+
let result = ""
|
|
34
|
+
let cursor = 0
|
|
35
|
+
|
|
36
|
+
while (cursor < template.length) {
|
|
37
|
+
const openIndex = template.indexOf("{", cursor)
|
|
38
|
+
if (openIndex === -1) {
|
|
39
|
+
result += template.slice(cursor)
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const closeIndex = template.indexOf("}", openIndex + 1)
|
|
44
|
+
if (closeIndex === -1) {
|
|
45
|
+
result += template.slice(cursor)
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
result += template.slice(cursor, openIndex)
|
|
50
|
+
const paramName = template.slice(openIndex + 1, closeIndex)
|
|
51
|
+
const value = params[paramName]
|
|
52
|
+
if (value === undefined) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Missing value for subject parameter "${paramName}" in template "${template}"`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if (!isValidSubjectValue(value)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Invalid value for subject parameter "${paramName}": "${value}" (must be non-empty and contain only letters, digits, "-", and "_")`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
result += value
|
|
63
|
+
|
|
64
|
+
cursor = closeIndex + 1
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED FILE - DO NOT EDIT.
|
|
3
|
+
* Generated from src/asyncapi.yaml by scripts/generate-nats-client.ts.
|
|
4
|
+
* Run `pnpm generate:nats` to regenerate.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
App,
|
|
9
|
+
AppCreatedEvent,
|
|
10
|
+
AppDeletedEvent,
|
|
11
|
+
AppUpdatedEvent,
|
|
12
|
+
BusIOsState,
|
|
13
|
+
Cell,
|
|
14
|
+
CellCreatedEvent,
|
|
15
|
+
CellCycleEvent,
|
|
16
|
+
CellDeletedEvent,
|
|
17
|
+
CellUpdatedEvent,
|
|
18
|
+
CollisionSetup,
|
|
19
|
+
ListIOValuesResponse,
|
|
20
|
+
MotionGroupDescription,
|
|
21
|
+
NatsErrorPayload,
|
|
22
|
+
NetworkStatusChangedEvent,
|
|
23
|
+
ProgramStatus,
|
|
24
|
+
RobotController,
|
|
25
|
+
RobotControllerCreatedEvent,
|
|
26
|
+
RobotControllerDeletedEvent,
|
|
27
|
+
RobotControllerState,
|
|
28
|
+
RobotControllerUpdatedEvent,
|
|
29
|
+
SelectIOs,
|
|
30
|
+
ServiceStatusList,
|
|
31
|
+
StreamIOValuesResponse,
|
|
32
|
+
SystemUpdateCompletedEvent,
|
|
33
|
+
SystemUpdateStartedEvent,
|
|
34
|
+
} from "./types.ts"
|
|
35
|
+
|
|
36
|
+
/** Subject parameters required by each NATS subject, e.g. "nova.v2.cells.{cell}". */
|
|
37
|
+
export interface NatsOperationParams {
|
|
38
|
+
/** publishCell */
|
|
39
|
+
"nova.v2.cells.{cell}": { cell: string }
|
|
40
|
+
/** publishApp */
|
|
41
|
+
"nova.v2.cells.{cell}.apps.{app}": { cell: string; app: string }
|
|
42
|
+
/** publishProgramStatus */
|
|
43
|
+
"nova.v2.cells.{cell}.programs": { cell: string }
|
|
44
|
+
/** publishRobotController */
|
|
45
|
+
"nova.v2.cells.{cell}.controllers.{controller}": {
|
|
46
|
+
cell: string
|
|
47
|
+
controller: string
|
|
48
|
+
}
|
|
49
|
+
/** publishCellStatus */
|
|
50
|
+
"nova.v2.cells.{cell}.status": { cell: string }
|
|
51
|
+
/** publishCellCycle */
|
|
52
|
+
"nova.v2.cells.{cell}.cycle": { cell: string }
|
|
53
|
+
/** publishSystemStatus */
|
|
54
|
+
"nova.v2.system.status": Record<string, never>
|
|
55
|
+
/** publishCollisionSetup */
|
|
56
|
+
"nova.v2.cells.{cell}.collision.setups.{setup}": {
|
|
57
|
+
cell: string
|
|
58
|
+
setup: string
|
|
59
|
+
}
|
|
60
|
+
/** publishBUSIOStatus */
|
|
61
|
+
"nova.v2.cells.{cell}.bus-ios.status": { cell: string }
|
|
62
|
+
/** publishBUSIOsIOs */
|
|
63
|
+
"nova.v2.cells.{cell}.bus-ios.ios": { cell: string }
|
|
64
|
+
/** setBUSIOsIOs */
|
|
65
|
+
"nova.v2.cells.{cell}.bus-ios.ios.set": { cell: string }
|
|
66
|
+
/** selectRobotControllerIOs */
|
|
67
|
+
"nova.v2.cells.{cell}.controllers.{controller}.ios.select": {
|
|
68
|
+
cell: string
|
|
69
|
+
controller: string
|
|
70
|
+
}
|
|
71
|
+
/** publishRobotControllerIOs */
|
|
72
|
+
"nova.v2.cells.{cell}.controllers.{controller}.ios": {
|
|
73
|
+
cell: string
|
|
74
|
+
controller: string
|
|
75
|
+
}
|
|
76
|
+
/** publishRobotControllersState */
|
|
77
|
+
"nova.v2.cells.{cell}.controllers.{controller}.state": {
|
|
78
|
+
cell: string
|
|
79
|
+
controller: string
|
|
80
|
+
}
|
|
81
|
+
/** publishMotionGroupDescription */
|
|
82
|
+
"nova.v2.cells.{cell}.controllers.{controller}.motion-groups.{motion-group}.description": {
|
|
83
|
+
cell: string
|
|
84
|
+
controller: string
|
|
85
|
+
"motion-group": string
|
|
86
|
+
}
|
|
87
|
+
/** eventSystemUpdateStarted */
|
|
88
|
+
"nova.v2.events.system.update.started": Record<string, never>
|
|
89
|
+
/** eventSystemUpdateCompleted */
|
|
90
|
+
"nova.v2.events.system.update.completed": Record<string, never>
|
|
91
|
+
/** eventSystemNetworkStatusChanged */
|
|
92
|
+
"nova.v2.events.system.network.status.changed": Record<string, never>
|
|
93
|
+
/** eventCellCreated */
|
|
94
|
+
"nova.v2.events.cells.{cell}.created": { cell: string }
|
|
95
|
+
/** eventCellUpdated */
|
|
96
|
+
"nova.v2.events.cells.{cell}.updated": { cell: string }
|
|
97
|
+
/** eventCellDeleted */
|
|
98
|
+
"nova.v2.events.cells.{cell}.deleted": { cell: string }
|
|
99
|
+
/** eventAppCreated */
|
|
100
|
+
"nova.v2.events.cells.{cell}.apps.{app}.created": {
|
|
101
|
+
cell: string
|
|
102
|
+
app: string
|
|
103
|
+
}
|
|
104
|
+
/** eventAppUpdated */
|
|
105
|
+
"nova.v2.events.cells.{cell}.apps.{app}.updated": {
|
|
106
|
+
cell: string
|
|
107
|
+
app: string
|
|
108
|
+
}
|
|
109
|
+
/** eventAppDeleted */
|
|
110
|
+
"nova.v2.events.cells.{cell}.apps.{app}.deleted": {
|
|
111
|
+
cell: string
|
|
112
|
+
app: string
|
|
113
|
+
}
|
|
114
|
+
/** eventRobotControllerCreated */
|
|
115
|
+
"nova.v2.events.cells.{cell}.controllers.{controller}.created": {
|
|
116
|
+
cell: string
|
|
117
|
+
controller: string
|
|
118
|
+
}
|
|
119
|
+
/** eventRobotControllerUpdated */
|
|
120
|
+
"nova.v2.events.cells.{cell}.controllers.{controller}.updated": {
|
|
121
|
+
cell: string
|
|
122
|
+
controller: string
|
|
123
|
+
}
|
|
124
|
+
/** eventRobotControllerDeleted */
|
|
125
|
+
"nova.v2.events.cells.{cell}.controllers.{controller}.deleted": {
|
|
126
|
+
cell: string
|
|
127
|
+
controller: string
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Payload types for subjects the server publishes and the client subscribes to. */
|
|
132
|
+
export interface NatsSubscribePayloads {
|
|
133
|
+
/** publishCell */
|
|
134
|
+
"nova.v2.cells.{cell}": Cell
|
|
135
|
+
/** publishApp */
|
|
136
|
+
"nova.v2.cells.{cell}.apps.{app}": App
|
|
137
|
+
/** publishProgramStatus */
|
|
138
|
+
"nova.v2.cells.{cell}.programs": ProgramStatus
|
|
139
|
+
/** publishRobotController */
|
|
140
|
+
"nova.v2.cells.{cell}.controllers.{controller}": RobotController
|
|
141
|
+
/** publishCellStatus */
|
|
142
|
+
"nova.v2.cells.{cell}.status": ServiceStatusList
|
|
143
|
+
/** publishCellCycle */
|
|
144
|
+
"nova.v2.cells.{cell}.cycle": CellCycleEvent
|
|
145
|
+
/** publishSystemStatus */
|
|
146
|
+
"nova.v2.system.status": ServiceStatusList
|
|
147
|
+
/** publishCollisionSetup */
|
|
148
|
+
"nova.v2.cells.{cell}.collision.setups.{setup}": CollisionSetup
|
|
149
|
+
/** publishBUSIOStatus */
|
|
150
|
+
"nova.v2.cells.{cell}.bus-ios.status": BusIOsState
|
|
151
|
+
/** publishBUSIOsIOs */
|
|
152
|
+
"nova.v2.cells.{cell}.bus-ios.ios": ListIOValuesResponse
|
|
153
|
+
/** publishRobotControllerIOs */
|
|
154
|
+
"nova.v2.cells.{cell}.controllers.{controller}.ios": StreamIOValuesResponse
|
|
155
|
+
/** publishRobotControllersState */
|
|
156
|
+
"nova.v2.cells.{cell}.controllers.{controller}.state": RobotControllerState
|
|
157
|
+
/** publishMotionGroupDescription */
|
|
158
|
+
"nova.v2.cells.{cell}.controllers.{controller}.motion-groups.{motion-group}.description": MotionGroupDescription
|
|
159
|
+
/** eventSystemUpdateStarted */
|
|
160
|
+
"nova.v2.events.system.update.started": SystemUpdateStartedEvent
|
|
161
|
+
/** eventSystemUpdateCompleted */
|
|
162
|
+
"nova.v2.events.system.update.completed": SystemUpdateCompletedEvent
|
|
163
|
+
/** eventSystemNetworkStatusChanged */
|
|
164
|
+
"nova.v2.events.system.network.status.changed": NetworkStatusChangedEvent
|
|
165
|
+
/** eventCellCreated */
|
|
166
|
+
"nova.v2.events.cells.{cell}.created": CellCreatedEvent
|
|
167
|
+
/** eventCellUpdated */
|
|
168
|
+
"nova.v2.events.cells.{cell}.updated": CellUpdatedEvent
|
|
169
|
+
/** eventCellDeleted */
|
|
170
|
+
"nova.v2.events.cells.{cell}.deleted": CellDeletedEvent
|
|
171
|
+
/** eventAppCreated */
|
|
172
|
+
"nova.v2.events.cells.{cell}.apps.{app}.created": AppCreatedEvent
|
|
173
|
+
/** eventAppUpdated */
|
|
174
|
+
"nova.v2.events.cells.{cell}.apps.{app}.updated": AppUpdatedEvent
|
|
175
|
+
/** eventAppDeleted */
|
|
176
|
+
"nova.v2.events.cells.{cell}.apps.{app}.deleted": AppDeletedEvent
|
|
177
|
+
/** eventRobotControllerCreated */
|
|
178
|
+
"nova.v2.events.cells.{cell}.controllers.{controller}.created": RobotControllerCreatedEvent
|
|
179
|
+
/** eventRobotControllerUpdated */
|
|
180
|
+
"nova.v2.events.cells.{cell}.controllers.{controller}.updated": RobotControllerUpdatedEvent
|
|
181
|
+
/** eventRobotControllerDeleted */
|
|
182
|
+
"nova.v2.events.cells.{cell}.controllers.{controller}.deleted": RobotControllerDeletedEvent
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export type NatsSubscribeSubject = keyof NatsSubscribePayloads
|
|
186
|
+
|
|
187
|
+
/** Request payload types for subjects the client sends requests to. */
|
|
188
|
+
export interface NatsRequestPayloads {
|
|
189
|
+
/** setBUSIOsIOs */
|
|
190
|
+
"nova.v2.cells.{cell}.bus-ios.ios.set": ListIOValuesResponse
|
|
191
|
+
/** selectRobotControllerIOs */
|
|
192
|
+
"nova.v2.cells.{cell}.controllers.{controller}.ios.select": SelectIOs
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Reply payload types for request/reply subjects. */
|
|
196
|
+
export interface NatsReplyPayloads {
|
|
197
|
+
/** setBUSIOsIOs */
|
|
198
|
+
"nova.v2.cells.{cell}.bus-ios.ios.set": NatsErrorPayload
|
|
199
|
+
/** selectRobotControllerIOs */
|
|
200
|
+
"nova.v2.cells.{cell}.controllers.{controller}.ios.select": NatsErrorPayload
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type NatsRequestSubject = keyof NatsRequestPayloads
|