@wandelbots/nova-js 4.4.0-pr.320.c17b82c → 4.4.0
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.d.mts +19 -25
- package/dist/experimental/nats/index.d.mts.map +1 -1
- package/dist/experimental/nats/index.mjs +13 -38
- package/dist/experimental/nats/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/experimental/nats/index.ts +4 -5
- package/src/lib/experimental/nats/NovaNatsClient.ts +41 -2
- package/src/lib/experimental/nats/matchSubject.ts +0 -49
|
@@ -11,29 +11,6 @@ import { ConnectionOptions, Msg, NatsConnection } from "@nats-io/nats-core";
|
|
|
11
11
|
*/
|
|
12
12
|
declare function buildNatsServerUrl(instanceUrl: string): string;
|
|
13
13
|
//#endregion
|
|
14
|
-
//#region src/lib/experimental/nats/matchSubject.d.ts
|
|
15
|
-
/**
|
|
16
|
-
* The inverse of `buildSubject`: extracts the `{param}` values of an
|
|
17
|
-
* AsyncAPI-style channel address template from a concrete NATS subject.
|
|
18
|
-
*
|
|
19
|
-
* Useful when subscribing with a wildcard param (e.g. `{ cell: "*" }`) to
|
|
20
|
-
* recover which entity a received message belongs to from `msg.subject`,
|
|
21
|
-
* instead of indexing into `subject.split(".")` by hand:
|
|
22
|
-
*
|
|
23
|
-
* matchSubject("nova.v2.cells.{cell}.status", "nova.v2.cells.a.status")
|
|
24
|
-
* // -> { cell: "a" }
|
|
25
|
-
*
|
|
26
|
-
* Matching is per dot-separated token: a `{param}` template token captures
|
|
27
|
-
* the concrete token under the param's name, a literal `*` token matches any
|
|
28
|
-
* single token without capturing, a trailing `>` token matches one or more
|
|
29
|
-
* remaining tokens, and any other token must match exactly. Placeholders
|
|
30
|
-
* embedded inside a larger token (e.g. `prefix-{param}`) are not supported.
|
|
31
|
-
*
|
|
32
|
-
* Returns the captured params (an empty object for a template without
|
|
33
|
-
* params), or `undefined` when the subject does not match the template.
|
|
34
|
-
*/
|
|
35
|
-
declare function matchSubject(template: string, subject: string): Record<string, string> | undefined;
|
|
36
|
-
//#endregion
|
|
37
14
|
//#region src/lib/experimental/nats/generated/types.d.ts
|
|
38
15
|
/**
|
|
39
16
|
* AUTO-GENERATED FILE - DO NOT EDIT.
|
|
@@ -3196,7 +3173,17 @@ type NatsPublishSubject = keyof NatsPublishPayloads;
|
|
|
3196
3173
|
//#endregion
|
|
3197
3174
|
//#region src/lib/experimental/nats/NovaNatsClient.d.ts
|
|
3198
3175
|
type NovaNatsClientConfig = ConnectionOptions;
|
|
3199
|
-
|
|
3176
|
+
/**
|
|
3177
|
+
* A received message, annotated with the values of the subject template's
|
|
3178
|
+
* `{param}` placeholders as extracted from the message's concrete subject.
|
|
3179
|
+
* With a wildcard subscription (e.g. `{ cell: "*" }`), `subjectParams` is how
|
|
3180
|
+
* a handler knows which entity a message belongs to. Typed per subject via
|
|
3181
|
+
* the generated `NatsOperationParams`.
|
|
3182
|
+
*/
|
|
3183
|
+
type NatsSubscribeMsg<K extends NatsSubscribeSubject> = Msg & {
|
|
3184
|
+
subjectParams: NatsOperationParams[K];
|
|
3185
|
+
};
|
|
3186
|
+
type NatsMessageHandler<K extends NatsSubscribeSubject> = (payload: NatsSubscribePayloads[K], msg: NatsSubscribeMsg<K>) => void | Promise<void>;
|
|
3200
3187
|
type SubscribeArgs<K extends NatsSubscribeSubject> = keyof NatsOperationParams[K] extends never ? [handler: NatsMessageHandler<K>] : [params: NatsOperationParams[K], handler: NatsMessageHandler<K>];
|
|
3201
3188
|
/**
|
|
3202
3189
|
* Typed NATS client for the Wandelbots NOVA messaging API, generated from
|
|
@@ -3228,6 +3215,13 @@ declare class NovaNatsClient {
|
|
|
3228
3215
|
* logged per-message, so one bad message doesn't stop later messages on
|
|
3229
3216
|
* the same subscription from being handled.
|
|
3230
3217
|
*
|
|
3218
|
+
* Each message is annotated with `msg.subjectParams` — the template's
|
|
3219
|
+
* `{param}` values extracted from the message's concrete subject — so a
|
|
3220
|
+
* wildcard subscriber knows which entity a message belongs to:
|
|
3221
|
+
*
|
|
3222
|
+
* nats.subscribe("nova.v2.cells.{cell}.status", { cell: "*" },
|
|
3223
|
+
* (services, msg) => console.log(msg.subjectParams.cell, services))
|
|
3224
|
+
*
|
|
3231
3225
|
* Returns a function that unsubscribes when called.
|
|
3232
3226
|
*/
|
|
3233
3227
|
subscribe<K extends NatsSubscribeSubject>(subject: K, ...args: SubscribeArgs<K>): Promise<() => void>;
|
|
@@ -3253,5 +3247,5 @@ declare class NovaNatsClient {
|
|
|
3253
3247
|
publish<K extends NatsPublishSubject>(subject: K, params: NatsOperationParams[K], payload: NatsPublishPayloads[K]): Promise<void>;
|
|
3254
3248
|
}
|
|
3255
3249
|
//#endregion
|
|
3256
|
-
export { type AbbController, type AddVirtualControllerMotionGroupRequest, type App, type AppCreatedEvent, type AppDeletedEvent, type AppEventData, type AppUpdatedEvent, type Box, type BusIOsState, type BusIOsStateEnum, type Capacity, type Capsule, type CartesianLimits, type Cell, type CellCreatedEvent, type CellCycleEvent, type CellDeletedEvent, type CellDescription, type CellEventData, type CellName, type CellUpdatedEvent, type CloudEvent, type Collider, type ColliderDictionary, type ColliderDictionary1, type ColliderDictionary2, type CollisionMotionGroupLink, type CollisionMotionGroupTool, type CollisionMotionGroupTool1, type CollisionSetup, type ContainerEnvironment, type ContainerImage, type ContainerResources, type ContainerStorage, type ControllerNetworkInterface, type ConvexHull, type Cylinder, type DHParameter, type Execute, type Execute1, type FanucController, type IOBooleanValue, type IOFloatValue, type IOIntegerValue, type IOValue, type ImageCredentials, type JoggingDetails, type JoggingPausedByUser, type JoggingPausedNearCollision, type JoggingPausedNearJointLimit, type JoggingPausedNearSingularity, type JoggingPausedOnIO, type JoggingRunning, type JointLimits, type JointTypeEnum, type Joints, type Joints1, type Joints2, type Joints3, type KukaController, type LimitRange, type LimitSet, type LinkChain, type LinkChain1, type ListIOValuesResponse, type Location, type Manufacturer, type MotionGroupDescription, type MotionGroupFromJSON, type MotionGroupFromModel, type MotionGroupModel, type MotionGroupState, type MotionGroupState1, type MotionGroupState_JointLimitReached, type MotionGroupState_JointLimitReached1, type NatsErrorPayload, type NatsOperationParams, type NatsPublishPayloads, type NatsPublishSubject, type NatsReplyPayloads, type NatsRequestPayloads, type NatsRequestSubject, type NatsSubscribePayloads, type NatsSubscribeSubject, type NetworkState, type NetworkStatusChangedEvent, NovaNatsClient, type NovaNatsClientConfig, type OperatingState, type OperationLimits, type OperationMode, type Payload, type PayloadDictionary, type Plane, type Pose, type Pose1, type Pose2, type Pose3, type Pose4, type Pose5, type ProgramRunState, type ProgramRunState1, type ProgramStatus, type Rectangle, type RectangularCapsule, type RobotController, type RobotControllerCreatedEvent, type RobotControllerDeletedEvent, type RobotControllerEventData, type RobotControllerState, type RobotControllerUpdatedEvent, type RobotSystemMode, type RotationVector, type SafetyStateType, type SafetyToolColliders, type SelectIOs, type ServiceGroup, type ServiceStatus, type ServiceStatusList, type ServiceStatusPhase, type ServiceStatusSeverity, type Sphere, type StreamIOValuesResponse, type SystemUpdateCompletedEvent, type SystemUpdateStartedEvent, type TcpOffset, type TcpOffsetDictionary, type TrajectoryDetails, type TrajectoryEnded, type TrajectoryPausedByUser, type TrajectoryPausedOnIO, type TrajectoryRunning, type TrajectoryWaitForIO, type UniversalrobotsController, type Vector3D, type Vector3D1, type Vector3D2, type VirtualController, type YaskawaController, buildNatsServerUrl
|
|
3250
|
+
export { type AbbController, type AddVirtualControllerMotionGroupRequest, type App, type AppCreatedEvent, type AppDeletedEvent, type AppEventData, type AppUpdatedEvent, type Box, type BusIOsState, type BusIOsStateEnum, type Capacity, type Capsule, type CartesianLimits, type Cell, type CellCreatedEvent, type CellCycleEvent, type CellDeletedEvent, type CellDescription, type CellEventData, type CellName, type CellUpdatedEvent, type CloudEvent, type Collider, type ColliderDictionary, type ColliderDictionary1, type ColliderDictionary2, type CollisionMotionGroupLink, type CollisionMotionGroupTool, type CollisionMotionGroupTool1, type CollisionSetup, type ContainerEnvironment, type ContainerImage, type ContainerResources, type ContainerStorage, type ControllerNetworkInterface, type ConvexHull, type Cylinder, type DHParameter, type Execute, type Execute1, type FanucController, type IOBooleanValue, type IOFloatValue, type IOIntegerValue, type IOValue, type ImageCredentials, type JoggingDetails, type JoggingPausedByUser, type JoggingPausedNearCollision, type JoggingPausedNearJointLimit, type JoggingPausedNearSingularity, type JoggingPausedOnIO, type JoggingRunning, type JointLimits, type JointTypeEnum, type Joints, type Joints1, type Joints2, type Joints3, type KukaController, type LimitRange, type LimitSet, type LinkChain, type LinkChain1, type ListIOValuesResponse, type Location, type Manufacturer, type MotionGroupDescription, type MotionGroupFromJSON, type MotionGroupFromModel, type MotionGroupModel, type MotionGroupState, type MotionGroupState1, type MotionGroupState_JointLimitReached, type MotionGroupState_JointLimitReached1, type NatsErrorPayload, type NatsOperationParams, type NatsPublishPayloads, type NatsPublishSubject, type NatsReplyPayloads, type NatsRequestPayloads, type NatsRequestSubject, type NatsSubscribeMsg, type NatsSubscribePayloads, type NatsSubscribeSubject, type NetworkState, type NetworkStatusChangedEvent, NovaNatsClient, type NovaNatsClientConfig, type OperatingState, type OperationLimits, type OperationMode, type Payload, type PayloadDictionary, type Plane, type Pose, type Pose1, type Pose2, type Pose3, type Pose4, type Pose5, type ProgramRunState, type ProgramRunState1, type ProgramStatus, type Rectangle, type RectangularCapsule, type RobotController, type RobotControllerCreatedEvent, type RobotControllerDeletedEvent, type RobotControllerEventData, type RobotControllerState, type RobotControllerUpdatedEvent, type RobotSystemMode, type RotationVector, type SafetyStateType, type SafetyToolColliders, type SelectIOs, type ServiceGroup, type ServiceStatus, type ServiceStatusList, type ServiceStatusPhase, type ServiceStatusSeverity, type Sphere, type StreamIOValuesResponse, type SystemUpdateCompletedEvent, type SystemUpdateStartedEvent, type TcpOffset, type TcpOffsetDictionary, type TrajectoryDetails, type TrajectoryEnded, type TrajectoryPausedByUser, type TrajectoryPausedOnIO, type TrajectoryRunning, type TrajectoryWaitForIO, type UniversalrobotsController, type Vector3D, type Vector3D1, type Vector3D2, type VirtualController, type YaskawaController, buildNatsServerUrl };
|
|
3257
3251
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/lib/experimental/nats/buildNatsServerUrl.ts","../../../src/lib/experimental/nats/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/lib/experimental/nats/buildNatsServerUrl.ts","../../../src/lib/experimental/nats/generated/types.ts","../../../src/lib/experimental/nats/generated/operations.ts","../../../src/lib/experimental/nats/NovaNatsClient.ts"],"mappings":"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"}
|
|
@@ -14,42 +14,6 @@ function buildNatsServerUrl(instanceUrl) {
|
|
|
14
14
|
return `${url.protocol === "https:" ? "wss:" : "ws:"}//${url.host}/api/nats`;
|
|
15
15
|
}
|
|
16
16
|
//#endregion
|
|
17
|
-
//#region src/lib/experimental/nats/matchSubject.ts
|
|
18
|
-
/**
|
|
19
|
-
* The inverse of `buildSubject`: extracts the `{param}` values of an
|
|
20
|
-
* AsyncAPI-style channel address template from a concrete NATS subject.
|
|
21
|
-
*
|
|
22
|
-
* Useful when subscribing with a wildcard param (e.g. `{ cell: "*" }`) to
|
|
23
|
-
* recover which entity a received message belongs to from `msg.subject`,
|
|
24
|
-
* instead of indexing into `subject.split(".")` by hand:
|
|
25
|
-
*
|
|
26
|
-
* matchSubject("nova.v2.cells.{cell}.status", "nova.v2.cells.a.status")
|
|
27
|
-
* // -> { cell: "a" }
|
|
28
|
-
*
|
|
29
|
-
* Matching is per dot-separated token: a `{param}` template token captures
|
|
30
|
-
* the concrete token under the param's name, a literal `*` token matches any
|
|
31
|
-
* single token without capturing, a trailing `>` token matches one or more
|
|
32
|
-
* remaining tokens, and any other token must match exactly. Placeholders
|
|
33
|
-
* embedded inside a larger token (e.g. `prefix-{param}`) are not supported.
|
|
34
|
-
*
|
|
35
|
-
* Returns the captured params (an empty object for a template without
|
|
36
|
-
* params), or `undefined` when the subject does not match the template.
|
|
37
|
-
*/
|
|
38
|
-
function matchSubject(template, subject) {
|
|
39
|
-
const templateTokens = template.split(".");
|
|
40
|
-
const subjectTokens = subject.split(".");
|
|
41
|
-
const params = {};
|
|
42
|
-
for (const [i, token] of templateTokens.entries()) {
|
|
43
|
-
if (token === ">" && i === templateTokens.length - 1) return subjectTokens.length > i ? params : void 0;
|
|
44
|
-
const subjectToken = subjectTokens[i];
|
|
45
|
-
if (subjectToken === void 0) return void 0;
|
|
46
|
-
if (token.startsWith("{") && token.endsWith("}") && token.length > 2) params[token.slice(1, -1)] = subjectToken;
|
|
47
|
-
else if (token !== "*" && token !== subjectToken) return;
|
|
48
|
-
}
|
|
49
|
-
if (subjectTokens.length !== templateTokens.length) return void 0;
|
|
50
|
-
return params;
|
|
51
|
-
}
|
|
52
|
-
//#endregion
|
|
53
17
|
//#region src/lib/experimental/nats/buildSubject.ts
|
|
54
18
|
/**
|
|
55
19
|
* Builds a NATS subject from an AsyncAPI-style channel address template
|
|
@@ -141,6 +105,13 @@ var NovaNatsClient = class {
|
|
|
141
105
|
* logged per-message, so one bad message doesn't stop later messages on
|
|
142
106
|
* the same subscription from being handled.
|
|
143
107
|
*
|
|
108
|
+
* Each message is annotated with `msg.subjectParams` — the template's
|
|
109
|
+
* `{param}` values extracted from the message's concrete subject — so a
|
|
110
|
+
* wildcard subscriber knows which entity a message belongs to:
|
|
111
|
+
*
|
|
112
|
+
* nats.subscribe("nova.v2.cells.{cell}.status", { cell: "*" },
|
|
113
|
+
* (services, msg) => console.log(msg.subjectParams.cell, services))
|
|
114
|
+
*
|
|
144
115
|
* Returns a function that unsubscribes when called.
|
|
145
116
|
*/
|
|
146
117
|
async subscribe(subject, ...args) {
|
|
@@ -148,9 +119,13 @@ var NovaNatsClient = class {
|
|
|
148
119
|
const nc = await this.connect();
|
|
149
120
|
const resolvedSubject = buildSubject(subject, params);
|
|
150
121
|
const sub = nc.subscribe(resolvedSubject);
|
|
122
|
+
const paramPositions = [];
|
|
123
|
+
for (const [index, token] of subject.split(".").entries()) if (token.startsWith("{") && token.endsWith("}")) paramPositions.push([token.slice(1, -1), index]);
|
|
151
124
|
(async () => {
|
|
152
125
|
for await (const msg of sub) try {
|
|
153
|
-
|
|
126
|
+
const subjectTokens = msg.subject.split(".");
|
|
127
|
+
const subjectParams = Object.fromEntries(paramPositions.map(([name, index]) => [name, subjectTokens[index] ?? ""]));
|
|
128
|
+
await handler(msg.json(), Object.assign(msg, { subjectParams }));
|
|
154
129
|
} catch (err) {
|
|
155
130
|
console.error(`Error handling NATS message on subject "${resolvedSubject}"`, err);
|
|
156
131
|
}
|
|
@@ -187,6 +162,6 @@ var NovaNatsClient = class {
|
|
|
187
162
|
}
|
|
188
163
|
};
|
|
189
164
|
//#endregion
|
|
190
|
-
export { NovaNatsClient, buildNatsServerUrl
|
|
165
|
+
export { NovaNatsClient, buildNatsServerUrl };
|
|
191
166
|
|
|
192
167
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/lib/experimental/nats/buildNatsServerUrl.ts","../../../src/lib/experimental/nats/matchSubject.ts","../../../src/lib/experimental/nats/buildSubject.ts","../../../src/lib/experimental/nats/NovaNatsClient.ts"],"sourcesContent":["import { parseNovaInstanceUrl } from \"../../converters.ts\"\n\n/**\n * Builds the WebSocket URL for a NOVA instance's NATS gateway from its\n * instance URL, e.g. `https://foo.instance.wandelbots.io` becomes\n * `wss://foo.instance.wandelbots.io/api/nats`.\n *\n * Pass the result as `servers` in the `NovaNatsClientConfig` passed to\n * `NovaNatsClient`.\n */\nexport function buildNatsServerUrl(instanceUrl: string): string {\n const url = parseNovaInstanceUrl(instanceUrl)\n const protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\"\n return `${protocol}//${url.host}/api/nats`\n}\n","/**\n * The inverse of `buildSubject`: extracts the `{param}` values of an\n * AsyncAPI-style channel address template from a concrete NATS subject.\n *\n * Useful when subscribing with a wildcard param (e.g. `{ cell: \"*\" }`) to\n * recover which entity a received message belongs to from `msg.subject`,\n * instead of indexing into `subject.split(\".\")` by hand:\n *\n * matchSubject(\"nova.v2.cells.{cell}.status\", \"nova.v2.cells.a.status\")\n * // -> { cell: \"a\" }\n *\n * Matching is per dot-separated token: a `{param}` template token captures\n * the concrete token under the param's name, a literal `*` token matches any\n * single token without capturing, a trailing `>` token matches one or more\n * remaining tokens, and any other token must match exactly. Placeholders\n * embedded inside a larger token (e.g. `prefix-{param}`) are not supported.\n *\n * Returns the captured params (an empty object for a template without\n * params), or `undefined` when the subject does not match the template.\n */\nexport function matchSubject(\n template: string,\n subject: string,\n): Record<string, string> | undefined {\n const templateTokens = template.split(\".\")\n const subjectTokens = subject.split(\".\")\n\n const params: Record<string, string> = {}\n\n for (const [i, token] of templateTokens.entries()) {\n if (token === \">\" && i === templateTokens.length - 1) {\n // `>` must match at least one remaining token.\n return subjectTokens.length > i ? params : undefined\n }\n\n const subjectToken = subjectTokens[i]\n if (subjectToken === undefined) return undefined\n\n if (token.startsWith(\"{\") && token.endsWith(\"}\") && token.length > 2) {\n params[token.slice(1, -1)] = subjectToken\n } else if (token !== \"*\" && token !== subjectToken) {\n return undefined\n }\n }\n\n if (subjectTokens.length !== templateTokens.length) return undefined\n\n return params\n}\n","/**\n * Builds a NATS subject from an AsyncAPI-style channel address template\n * (e.g. `\"{instance}.v2.cells.{cell}\"`) by substituting each `{param}`\n * placeholder with the corresponding value from `params`.\n */\n\nfunction isValidSubjectChar(char: string): boolean {\n const code = char.charCodeAt(0)\n return (\n (code >= 48 && code <= 57) || // 0-9\n (code >= 65 && code <= 90) || // A-Z\n (code >= 97 && code <= 122) || // a-z\n char === \"-\" ||\n char === \"_\"\n )\n}\n\nfunction isValidSubjectValue(value: string): boolean {\n // A bare \"*\" is the NATS single-token wildcard, allowed on its own (e.g.\n // subscribing to all cells with `{ cell: \"*\" }\") but not as part of a\n // larger value, since it wouldn't act as a wildcard there anyway.\n if (value === \"*\") return true\n if (value.length === 0) return false\n for (const char of value) {\n if (!isValidSubjectChar(char)) return false\n }\n return true\n}\n\nexport function buildSubject(\n template: string,\n params: Record<string, string>,\n): string {\n // Scanned manually (rather than with a regex like /\\{([^}]+)\\}/g) to avoid\n // a polynomial-time backtracking blowup on pathological input, e.g. a\n // template consisting of many \"{\" characters with no closing \"}\".\n let result = \"\"\n let cursor = 0\n\n while (cursor < template.length) {\n const openIndex = template.indexOf(\"{\", cursor)\n if (openIndex === -1) {\n result += template.slice(cursor)\n break\n }\n\n const closeIndex = template.indexOf(\"}\", openIndex + 1)\n if (closeIndex === -1) {\n result += template.slice(cursor)\n break\n }\n\n result += template.slice(cursor, openIndex)\n const paramName = template.slice(openIndex + 1, closeIndex)\n const value = params[paramName]\n if (value === undefined) {\n throw new Error(\n `Missing value for subject parameter \"${paramName}\" in template \"${template}\"`,\n )\n }\n if (!isValidSubjectValue(value)) {\n throw new Error(\n `Invalid value for subject parameter \"${paramName}\": \"${value}\" (must be non-empty and contain only letters, digits, \"-\", and \"_\")`,\n )\n }\n result += value\n\n cursor = closeIndex + 1\n }\n\n return result\n}\n","import {\n type ConnectionOptions,\n type Msg,\n type NatsConnection,\n wsconnect,\n} from \"@nats-io/nats-core\"\nimport type { Nova } from \"../../Nova.ts\"\nimport { buildNatsServerUrl } from \"./buildNatsServerUrl.ts\"\nimport { buildSubject } from \"./buildSubject.ts\"\nimport type {\n NatsOperationParams,\n NatsPublishPayloads,\n NatsPublishSubject,\n NatsReplyPayloads,\n NatsRequestPayloads,\n NatsRequestSubject,\n NatsSubscribePayloads,\n NatsSubscribeSubject,\n} from \"./generated/operations.ts\"\n\nexport type NovaNatsClientConfig = ConnectionOptions\n\ntype NatsMessageHandler<K extends NatsSubscribeSubject> = (\n payload: NatsSubscribePayloads[K],\n msg: Msg,\n) => void | Promise<void>\n\ntype SubscribeArgs<K extends NatsSubscribeSubject> =\n keyof NatsOperationParams[K] extends never\n ? [handler: NatsMessageHandler<K>]\n : [params: NatsOperationParams[K], handler: NatsMessageHandler<K>]\n\n/**\n * Typed NATS client for the Wandelbots NOVA messaging API, generated from\n * src/asyncapi.yaml (see scripts/generate-nats-client.ts).\n *\n * Connects over WebSocket via `@nats-io/nats-core`'s `wsconnect`.\n */\nexport class NovaNatsClient {\n readonly config: NovaNatsClientConfig\n private connectionPromise: Promise<NatsConnection> | null = null\n\n constructor(nova: Nova, config: NovaNatsClientConfig = {}) {\n this.config = {\n servers: buildNatsServerUrl(nova.instanceUrl.href),\n // Reuse the Nova instance's access token for NATS auth, if it has one\n // (e.g. from login or a passed-in config.accessToken). Explicit auth\n // options in `config` (token/user/pass/authenticator) still win.\n ...(nova.accessToken ? { token: nova.accessToken } : {}),\n ...config,\n }\n }\n\n /**\n * Connects to NATS if not already connected or connecting, and returns the\n * connection. Safe to call concurrently: all callers share the same\n * in-flight connection attempt instead of each starting their own.\n */\n connect(): Promise<NatsConnection> {\n if (!this.connectionPromise) {\n this.connectionPromise = wsconnect(this.config).catch((err: unknown) => {\n // Allow a subsequent connect() call to retry after a failed attempt.\n this.connectionPromise = null\n throw err\n })\n }\n return this.connectionPromise\n }\n\n /** Closes the underlying NATS connection, if open or connecting. */\n async close(): Promise<void> {\n const connectionPromise = this.connectionPromise\n this.connectionPromise = null\n if (!connectionPromise) return\n try {\n const nc = await connectionPromise\n await nc.close()\n } catch {\n // Connection never succeeded; nothing to close.\n }\n }\n\n /**\n * Subscribes to a NATS subject published by the server, invoking `handler`\n * with the JSON-decoded payload of every message received.\n *\n * `subject` is the subject template as it appears on the wire, e.g.\n * `\"nova.v2.cells.{cell}\"`, with `{param}` placeholders filled in from\n * `params`.\n *\n * Errors decoding a message or thrown/rejected by `handler` are caught and\n * logged per-message, so one bad message doesn't stop later messages on\n * the same subscription from being handled.\n *\n * Returns a function that unsubscribes when called.\n */\n async subscribe<K extends NatsSubscribeSubject>(\n subject: K,\n ...args: SubscribeArgs<K>\n ): Promise<() => void> {\n const [params, handler] =\n args.length === 1\n ? ([{}, args[0]] as const)\n : ([args[0], args[1]] as const)\n\n const nc = await this.connect()\n const resolvedSubject = buildSubject(subject, params)\n const sub = nc.subscribe(resolvedSubject)\n\n ;(async () => {\n for await (const msg of sub) {\n // Handled per-message: a bad payload or a throwing/rejecting handler\n // should not stop the subscription from processing later messages.\n try {\n await handler(msg.json<NatsSubscribePayloads[K]>(), msg)\n } catch (err) {\n console.error(\n `Error handling NATS message on subject \"${resolvedSubject}\"`,\n err,\n )\n }\n }\n })().catch((err: unknown) => {\n console.error(\n `NATS subscription iterator failed for \"${resolvedSubject}\"`,\n err,\n )\n })\n\n return () => sub.unsubscribe()\n }\n\n /**\n * Sends a request payload for a NATS subject the server receives, and\n * waits for the JSON-decoded reply.\n *\n * `subject` is the subject template as it appears on the wire, e.g.\n * `\"nova.v2.cells.{cell}.bus-ios.ios.set\"`, with `{param}` placeholders\n * filled in from `params`.\n */\n async request<K extends NatsRequestSubject>(\n subject: K,\n params: NatsOperationParams[K],\n payload: NatsRequestPayloads[K],\n opts: { timeout?: number } = {},\n ): Promise<NatsReplyPayloads[K]> {\n const nc = await this.connect()\n const resolvedSubject = buildSubject(subject, params)\n const msg = await nc.request(resolvedSubject, JSON.stringify(payload), {\n timeout: opts.timeout ?? 5000,\n })\n return msg.json<NatsReplyPayloads[K]>()\n }\n\n /**\n * Publishes a JSON payload to any NATS subject defined in the spec,\n * without waiting for a reply.\n *\n * `subject` is the subject template as it appears on the wire, e.g.\n * `\"nova.v2.cells.{cell}.bus-ios.ios.set\"`, with `{param}` placeholders\n * filled in from `params`.\n */\n async publish<K extends NatsPublishSubject>(\n subject: K,\n params: NatsOperationParams[K],\n payload: NatsPublishPayloads[K],\n ): Promise<void> {\n const nc = await this.connect()\n const resolvedSubject = buildSubject(subject, params)\n nc.publish(resolvedSubject, JSON.stringify(payload))\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/lib/experimental/nats/buildNatsServerUrl.ts","../../../src/lib/experimental/nats/buildSubject.ts","../../../src/lib/experimental/nats/NovaNatsClient.ts"],"sourcesContent":["import { parseNovaInstanceUrl } from \"../../converters.ts\"\n\n/**\n * Builds the WebSocket URL for a NOVA instance's NATS gateway from its\n * instance URL, e.g. `https://foo.instance.wandelbots.io` becomes\n * `wss://foo.instance.wandelbots.io/api/nats`.\n *\n * Pass the result as `servers` in the `NovaNatsClientConfig` passed to\n * `NovaNatsClient`.\n */\nexport function buildNatsServerUrl(instanceUrl: string): string {\n const url = parseNovaInstanceUrl(instanceUrl)\n const protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\"\n return `${protocol}//${url.host}/api/nats`\n}\n","/**\n * Builds a NATS subject from an AsyncAPI-style channel address template\n * (e.g. `\"{instance}.v2.cells.{cell}\"`) by substituting each `{param}`\n * placeholder with the corresponding value from `params`.\n */\n\nfunction isValidSubjectChar(char: string): boolean {\n const code = char.charCodeAt(0)\n return (\n (code >= 48 && code <= 57) || // 0-9\n (code >= 65 && code <= 90) || // A-Z\n (code >= 97 && code <= 122) || // a-z\n char === \"-\" ||\n char === \"_\"\n )\n}\n\nfunction isValidSubjectValue(value: string): boolean {\n // A bare \"*\" is the NATS single-token wildcard, allowed on its own (e.g.\n // subscribing to all cells with `{ cell: \"*\" }\") but not as part of a\n // larger value, since it wouldn't act as a wildcard there anyway.\n if (value === \"*\") return true\n if (value.length === 0) return false\n for (const char of value) {\n if (!isValidSubjectChar(char)) return false\n }\n return true\n}\n\nexport function buildSubject(\n template: string,\n params: Record<string, string>,\n): string {\n // Scanned manually (rather than with a regex like /\\{([^}]+)\\}/g) to avoid\n // a polynomial-time backtracking blowup on pathological input, e.g. a\n // template consisting of many \"{\" characters with no closing \"}\".\n let result = \"\"\n let cursor = 0\n\n while (cursor < template.length) {\n const openIndex = template.indexOf(\"{\", cursor)\n if (openIndex === -1) {\n result += template.slice(cursor)\n break\n }\n\n const closeIndex = template.indexOf(\"}\", openIndex + 1)\n if (closeIndex === -1) {\n result += template.slice(cursor)\n break\n }\n\n result += template.slice(cursor, openIndex)\n const paramName = template.slice(openIndex + 1, closeIndex)\n const value = params[paramName]\n if (value === undefined) {\n throw new Error(\n `Missing value for subject parameter \"${paramName}\" in template \"${template}\"`,\n )\n }\n if (!isValidSubjectValue(value)) {\n throw new Error(\n `Invalid value for subject parameter \"${paramName}\": \"${value}\" (must be non-empty and contain only letters, digits, \"-\", and \"_\")`,\n )\n }\n result += value\n\n cursor = closeIndex + 1\n }\n\n return result\n}\n","import {\n type ConnectionOptions,\n type Msg,\n type NatsConnection,\n wsconnect,\n} from \"@nats-io/nats-core\"\nimport type { Nova } from \"../../Nova.ts\"\nimport { buildNatsServerUrl } from \"./buildNatsServerUrl.ts\"\nimport { buildSubject } from \"./buildSubject.ts\"\nimport type {\n NatsOperationParams,\n NatsPublishPayloads,\n NatsPublishSubject,\n NatsReplyPayloads,\n NatsRequestPayloads,\n NatsRequestSubject,\n NatsSubscribePayloads,\n NatsSubscribeSubject,\n} from \"./generated/operations.ts\"\n\nexport type NovaNatsClientConfig = ConnectionOptions\n\n/**\n * A received message, annotated with the values of the subject template's\n * `{param}` placeholders as extracted from the message's concrete subject.\n * With a wildcard subscription (e.g. `{ cell: \"*\" }`), `subjectParams` is how\n * a handler knows which entity a message belongs to. Typed per subject via\n * the generated `NatsOperationParams`.\n */\nexport type NatsSubscribeMsg<K extends NatsSubscribeSubject> = Msg & {\n subjectParams: NatsOperationParams[K]\n}\n\ntype NatsMessageHandler<K extends NatsSubscribeSubject> = (\n payload: NatsSubscribePayloads[K],\n msg: NatsSubscribeMsg<K>,\n) => void | Promise<void>\n\ntype SubscribeArgs<K extends NatsSubscribeSubject> =\n keyof NatsOperationParams[K] extends never\n ? [handler: NatsMessageHandler<K>]\n : [params: NatsOperationParams[K], handler: NatsMessageHandler<K>]\n\n/**\n * Typed NATS client for the Wandelbots NOVA messaging API, generated from\n * src/asyncapi.yaml (see scripts/generate-nats-client.ts).\n *\n * Connects over WebSocket via `@nats-io/nats-core`'s `wsconnect`.\n */\nexport class NovaNatsClient {\n readonly config: NovaNatsClientConfig\n private connectionPromise: Promise<NatsConnection> | null = null\n\n constructor(nova: Nova, config: NovaNatsClientConfig = {}) {\n this.config = {\n servers: buildNatsServerUrl(nova.instanceUrl.href),\n // Reuse the Nova instance's access token for NATS auth, if it has one\n // (e.g. from login or a passed-in config.accessToken). Explicit auth\n // options in `config` (token/user/pass/authenticator) still win.\n ...(nova.accessToken ? { token: nova.accessToken } : {}),\n ...config,\n }\n }\n\n /**\n * Connects to NATS if not already connected or connecting, and returns the\n * connection. Safe to call concurrently: all callers share the same\n * in-flight connection attempt instead of each starting their own.\n */\n connect(): Promise<NatsConnection> {\n if (!this.connectionPromise) {\n this.connectionPromise = wsconnect(this.config).catch((err: unknown) => {\n // Allow a subsequent connect() call to retry after a failed attempt.\n this.connectionPromise = null\n throw err\n })\n }\n return this.connectionPromise\n }\n\n /** Closes the underlying NATS connection, if open or connecting. */\n async close(): Promise<void> {\n const connectionPromise = this.connectionPromise\n this.connectionPromise = null\n if (!connectionPromise) return\n try {\n const nc = await connectionPromise\n await nc.close()\n } catch {\n // Connection never succeeded; nothing to close.\n }\n }\n\n /**\n * Subscribes to a NATS subject published by the server, invoking `handler`\n * with the JSON-decoded payload of every message received.\n *\n * `subject` is the subject template as it appears on the wire, e.g.\n * `\"nova.v2.cells.{cell}\"`, with `{param}` placeholders filled in from\n * `params`.\n *\n * Errors decoding a message or thrown/rejected by `handler` are caught and\n * logged per-message, so one bad message doesn't stop later messages on\n * the same subscription from being handled.\n *\n * Each message is annotated with `msg.subjectParams` — the template's\n * `{param}` values extracted from the message's concrete subject — so a\n * wildcard subscriber knows which entity a message belongs to:\n *\n * nats.subscribe(\"nova.v2.cells.{cell}.status\", { cell: \"*\" },\n * (services, msg) => console.log(msg.subjectParams.cell, services))\n *\n * Returns a function that unsubscribes when called.\n */\n async subscribe<K extends NatsSubscribeSubject>(\n subject: K,\n ...args: SubscribeArgs<K>\n ): Promise<() => void> {\n const [params, handler] =\n args.length === 1\n ? ([{}, args[0]] as const)\n : ([args[0], args[1]] as const)\n\n const nc = await this.connect()\n const resolvedSubject = buildSubject(subject, params)\n const sub = nc.subscribe(resolvedSubject)\n\n // The token positions of the template's {param} placeholders, computed\n // once here so each message's subjectParams is a plain index pick: a\n // delivered message always matches the subscribed pattern token for\n // token, so its params sit at the same positions as in the template.\n const paramPositions: [name: string, index: number][] = []\n for (const [index, token] of subject.split(\".\").entries()) {\n if (token.startsWith(\"{\") && token.endsWith(\"}\")) {\n paramPositions.push([token.slice(1, -1), index])\n }\n }\n\n ;(async () => {\n for await (const msg of sub) {\n // Handled per-message: a bad payload or a throwing/rejecting handler\n // should not stop the subscription from processing later messages.\n try {\n const subjectTokens = msg.subject.split(\".\")\n const subjectParams = Object.fromEntries(\n paramPositions.map(([name, index]) => [\n name,\n subjectTokens[index] ?? \"\",\n ]),\n ) as NatsOperationParams[K]\n await handler(\n msg.json<NatsSubscribePayloads[K]>(),\n Object.assign(msg, { subjectParams }),\n )\n } catch (err) {\n console.error(\n `Error handling NATS message on subject \"${resolvedSubject}\"`,\n err,\n )\n }\n }\n })().catch((err: unknown) => {\n console.error(\n `NATS subscription iterator failed for \"${resolvedSubject}\"`,\n err,\n )\n })\n\n return () => sub.unsubscribe()\n }\n\n /**\n * Sends a request payload for a NATS subject the server receives, and\n * waits for the JSON-decoded reply.\n *\n * `subject` is the subject template as it appears on the wire, e.g.\n * `\"nova.v2.cells.{cell}.bus-ios.ios.set\"`, with `{param}` placeholders\n * filled in from `params`.\n */\n async request<K extends NatsRequestSubject>(\n subject: K,\n params: NatsOperationParams[K],\n payload: NatsRequestPayloads[K],\n opts: { timeout?: number } = {},\n ): Promise<NatsReplyPayloads[K]> {\n const nc = await this.connect()\n const resolvedSubject = buildSubject(subject, params)\n const msg = await nc.request(resolvedSubject, JSON.stringify(payload), {\n timeout: opts.timeout ?? 5000,\n })\n return msg.json<NatsReplyPayloads[K]>()\n }\n\n /**\n * Publishes a JSON payload to any NATS subject defined in the spec,\n * without waiting for a reply.\n *\n * `subject` is the subject template as it appears on the wire, e.g.\n * `\"nova.v2.cells.{cell}.bus-ios.ios.set\"`, with `{param}` placeholders\n * filled in from `params`.\n */\n async publish<K extends NatsPublishSubject>(\n subject: K,\n params: NatsOperationParams[K],\n payload: NatsPublishPayloads[K],\n ): Promise<void> {\n const nc = await this.connect()\n const resolvedSubject = buildSubject(subject, params)\n nc.publish(resolvedSubject, JSON.stringify(payload))\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wandelbots/nova-js",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.4.0
|
|
4
|
+
"version": "4.4.0",
|
|
5
5
|
"description": "Official JS client for the Wandelbots API",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
|
@@ -5,10 +5,6 @@
|
|
|
5
5
|
* This API is experimental and may change without a major version bump.
|
|
6
6
|
*/
|
|
7
7
|
export { buildNatsServerUrl } from "../../lib/experimental/nats/buildNatsServerUrl.ts"
|
|
8
|
-
// Extract the `{param}` values of a channel template back out of a received
|
|
9
|
-
// message's concrete subject (the inverse of what the client does when
|
|
10
|
-
// subscribing) — needed by consumers subscribing with wildcard params.
|
|
11
|
-
export { matchSubject } from "../../lib/experimental/nats/matchSubject.ts"
|
|
12
8
|
export type {
|
|
13
9
|
NatsOperationParams,
|
|
14
10
|
NatsPublishPayloads,
|
|
@@ -24,4 +20,7 @@ export type {
|
|
|
24
20
|
// which type applies to which subject).
|
|
25
21
|
export type * from "../../lib/experimental/nats/generated/types.ts"
|
|
26
22
|
export { NovaNatsClient } from "../../lib/experimental/nats/NovaNatsClient.ts"
|
|
27
|
-
export type {
|
|
23
|
+
export type {
|
|
24
|
+
NatsSubscribeMsg,
|
|
25
|
+
NovaNatsClientConfig,
|
|
26
|
+
} from "../../lib/experimental/nats/NovaNatsClient.ts"
|
|
@@ -20,9 +20,20 @@ import type {
|
|
|
20
20
|
|
|
21
21
|
export type NovaNatsClientConfig = ConnectionOptions
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A received message, annotated with the values of the subject template's
|
|
25
|
+
* `{param}` placeholders as extracted from the message's concrete subject.
|
|
26
|
+
* With a wildcard subscription (e.g. `{ cell: "*" }`), `subjectParams` is how
|
|
27
|
+
* a handler knows which entity a message belongs to. Typed per subject via
|
|
28
|
+
* the generated `NatsOperationParams`.
|
|
29
|
+
*/
|
|
30
|
+
export type NatsSubscribeMsg<K extends NatsSubscribeSubject> = Msg & {
|
|
31
|
+
subjectParams: NatsOperationParams[K]
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
type NatsMessageHandler<K extends NatsSubscribeSubject> = (
|
|
24
35
|
payload: NatsSubscribePayloads[K],
|
|
25
|
-
msg:
|
|
36
|
+
msg: NatsSubscribeMsg<K>,
|
|
26
37
|
) => void | Promise<void>
|
|
27
38
|
|
|
28
39
|
type SubscribeArgs<K extends NatsSubscribeSubject> =
|
|
@@ -92,6 +103,13 @@ export class NovaNatsClient {
|
|
|
92
103
|
* logged per-message, so one bad message doesn't stop later messages on
|
|
93
104
|
* the same subscription from being handled.
|
|
94
105
|
*
|
|
106
|
+
* Each message is annotated with `msg.subjectParams` — the template's
|
|
107
|
+
* `{param}` values extracted from the message's concrete subject — so a
|
|
108
|
+
* wildcard subscriber knows which entity a message belongs to:
|
|
109
|
+
*
|
|
110
|
+
* nats.subscribe("nova.v2.cells.{cell}.status", { cell: "*" },
|
|
111
|
+
* (services, msg) => console.log(msg.subjectParams.cell, services))
|
|
112
|
+
*
|
|
95
113
|
* Returns a function that unsubscribes when called.
|
|
96
114
|
*/
|
|
97
115
|
async subscribe<K extends NatsSubscribeSubject>(
|
|
@@ -107,12 +125,33 @@ export class NovaNatsClient {
|
|
|
107
125
|
const resolvedSubject = buildSubject(subject, params)
|
|
108
126
|
const sub = nc.subscribe(resolvedSubject)
|
|
109
127
|
|
|
128
|
+
// The token positions of the template's {param} placeholders, computed
|
|
129
|
+
// once here so each message's subjectParams is a plain index pick: a
|
|
130
|
+
// delivered message always matches the subscribed pattern token for
|
|
131
|
+
// token, so its params sit at the same positions as in the template.
|
|
132
|
+
const paramPositions: [name: string, index: number][] = []
|
|
133
|
+
for (const [index, token] of subject.split(".").entries()) {
|
|
134
|
+
if (token.startsWith("{") && token.endsWith("}")) {
|
|
135
|
+
paramPositions.push([token.slice(1, -1), index])
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
110
139
|
;(async () => {
|
|
111
140
|
for await (const msg of sub) {
|
|
112
141
|
// Handled per-message: a bad payload or a throwing/rejecting handler
|
|
113
142
|
// should not stop the subscription from processing later messages.
|
|
114
143
|
try {
|
|
115
|
-
|
|
144
|
+
const subjectTokens = msg.subject.split(".")
|
|
145
|
+
const subjectParams = Object.fromEntries(
|
|
146
|
+
paramPositions.map(([name, index]) => [
|
|
147
|
+
name,
|
|
148
|
+
subjectTokens[index] ?? "",
|
|
149
|
+
]),
|
|
150
|
+
) as NatsOperationParams[K]
|
|
151
|
+
await handler(
|
|
152
|
+
msg.json<NatsSubscribePayloads[K]>(),
|
|
153
|
+
Object.assign(msg, { subjectParams }),
|
|
154
|
+
)
|
|
116
155
|
} catch (err) {
|
|
117
156
|
console.error(
|
|
118
157
|
`Error handling NATS message on subject "${resolvedSubject}"`,
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The inverse of `buildSubject`: extracts the `{param}` values of an
|
|
3
|
-
* AsyncAPI-style channel address template from a concrete NATS subject.
|
|
4
|
-
*
|
|
5
|
-
* Useful when subscribing with a wildcard param (e.g. `{ cell: "*" }`) to
|
|
6
|
-
* recover which entity a received message belongs to from `msg.subject`,
|
|
7
|
-
* instead of indexing into `subject.split(".")` by hand:
|
|
8
|
-
*
|
|
9
|
-
* matchSubject("nova.v2.cells.{cell}.status", "nova.v2.cells.a.status")
|
|
10
|
-
* // -> { cell: "a" }
|
|
11
|
-
*
|
|
12
|
-
* Matching is per dot-separated token: a `{param}` template token captures
|
|
13
|
-
* the concrete token under the param's name, a literal `*` token matches any
|
|
14
|
-
* single token without capturing, a trailing `>` token matches one or more
|
|
15
|
-
* remaining tokens, and any other token must match exactly. Placeholders
|
|
16
|
-
* embedded inside a larger token (e.g. `prefix-{param}`) are not supported.
|
|
17
|
-
*
|
|
18
|
-
* Returns the captured params (an empty object for a template without
|
|
19
|
-
* params), or `undefined` when the subject does not match the template.
|
|
20
|
-
*/
|
|
21
|
-
export function matchSubject(
|
|
22
|
-
template: string,
|
|
23
|
-
subject: string,
|
|
24
|
-
): Record<string, string> | undefined {
|
|
25
|
-
const templateTokens = template.split(".")
|
|
26
|
-
const subjectTokens = subject.split(".")
|
|
27
|
-
|
|
28
|
-
const params: Record<string, string> = {}
|
|
29
|
-
|
|
30
|
-
for (const [i, token] of templateTokens.entries()) {
|
|
31
|
-
if (token === ">" && i === templateTokens.length - 1) {
|
|
32
|
-
// `>` must match at least one remaining token.
|
|
33
|
-
return subjectTokens.length > i ? params : undefined
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const subjectToken = subjectTokens[i]
|
|
37
|
-
if (subjectToken === undefined) return undefined
|
|
38
|
-
|
|
39
|
-
if (token.startsWith("{") && token.endsWith("}") && token.length > 2) {
|
|
40
|
-
params[token.slice(1, -1)] = subjectToken
|
|
41
|
-
} else if (token !== "*" && token !== subjectToken) {
|
|
42
|
-
return undefined
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (subjectTokens.length !== templateTokens.length) return undefined
|
|
47
|
-
|
|
48
|
-
return params
|
|
49
|
-
}
|