@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.
@@ -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