Skip to content

Commit 292d170

Browse files
committed
feat(js): recover javascript wrapper, test passing
1 parent 131eee4 commit 292d170

File tree

6 files changed

+201
-88
lines changed

6 files changed

+201
-88
lines changed
Lines changed: 71 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
#include <emscripten/bind.h>
2+
#include <emscripten/val.h>
23

3-
#include "RtBot.pb.h"
44
#include "rtbot/Message.h"
5+
#include "rtbot/OperatorJson.h"
56
#include "rtbot/bindings.h"
67

78
using namespace emscripten;
9+
using json = nlohmann::json;
10+
using timestamp_t = rtbot::timestamp_t;
811

912
namespace emscripten {
1013
namespace internal {
1114

15+
// Vector binding type for converting between C++ vectors and JavaScript arrays
1216
template <typename T, typename Allocator>
1317
struct BindingType<std::vector<T, Allocator>> {
1418
using ValBinding = BindingType<val>;
@@ -21,6 +25,7 @@ struct BindingType<std::vector<T, Allocator>> {
2125
}
2226
};
2327

28+
// TypeID specialization for vectors to ensure proper type handling
2429
template <typename T>
2530
struct TypeID<
2631
T, typename std::enable_if_t<std::is_same<typename Canonicalized<T>::type,
@@ -32,41 +37,77 @@ struct TypeID<
3237
} // namespace internal
3338
} // namespace emscripten
3439

35-
void test(rtbot::api::proto::Input const& input) { std::cout << "Input: " << input.DebugString() << std::endl; }
36-
37-
string processBatch32(string const& programId, vector<uint32_t> times32, vector<double> values,
38-
vector<string> const& ports) {
39-
// translate passed 32 bit timestamp into 64 bit internal type
40-
vector<uint64_t> times(times32.begin(), times32.end());
41-
return processBatch(programId, times, values, ports);
40+
// Helper function to process batches with 32-bit timestamps
41+
std::string processBatch32(const std::string& programId, const std::vector<uint32_t>& times32,
42+
const std::vector<double>& values, const std::vector<std::string>& ports) {
43+
// Convert 32-bit timestamps to 64-bit
44+
std::vector<uint64_t> times(times32.begin(), times32.end());
45+
return rtbot::process_batch(programId, times, values, ports);
4246
}
4347

44-
string processBatch32Debug(string const& programId, vector<uint32_t> times32, vector<double> values,
45-
vector<string> const& ports) {
46-
// translate passed 32 bit timestamp into 64 bit internal type
47-
vector<uint64_t> times(times32.begin(), times32.end());
48-
return processBatchDebug(programId, times, values, ports);
48+
// Debug version of batch processing
49+
std::string processBatch32Debug(const std::string& programId, const std::vector<uint32_t>& times32,
50+
const std::vector<double>& values, const std::vector<std::string>& ports) {
51+
std::vector<uint64_t> times(times32.begin(), times32.end());
52+
return rtbot::process_batch_debug(programId, times, values, ports);
4953
}
5054

51-
EMSCRIPTEN_BINDINGS(RtBot) {
52-
value_object<rtbot::Message<std::uint64_t, double>>("Message")
53-
.field("time", &rtbot::Message<std::uint64_t, double>::time)
54-
.field("value", &rtbot::Message<std::uint64_t, double>::value);
55+
// Helper to add a single message to the buffer
56+
std::string addMessage(const std::string& programId, const std::string& portId, uint32_t time, double value) {
57+
return rtbot::add_to_message_buffer(programId, portId, static_cast<uint64_t>(time), value);
58+
}
5559

56-
emscripten::function("validate", &validate);
57-
emscripten::function("validateOperator", &validateOperator);
60+
namespace {
61+
// Helper functions for message creation and access
62+
timestamp_t getMessage_getTime(const rtbot::Message<rtbot::NumberData>& msg) { return msg.time; }
5863

59-
emscripten::function("createProgram", &createProgram);
60-
emscripten::function("deleteProgram", &deleteProgram);
64+
void getMessage_setTime(rtbot::Message<rtbot::NumberData>& msg, timestamp_t t) { msg.time = t; }
6165

62-
emscripten::function("addToMessageBuffer", &addToMessageBuffer);
63-
emscripten::function("processMessageBuffer", &processMessageBuffer);
64-
emscripten::function("processMessageBufferDebug", &processMessageBufferDebug);
66+
const rtbot::NumberData& getMessage_getData(const rtbot::Message<rtbot::NumberData>& msg) { return msg.data; }
6567

66-
emscripten::function("getProgramEntryOperatorId", &getProgramEntryOperatorId);
67-
emscripten::function("getProgramEntryPorts", &getProgramEntryPorts);
68-
emscripten::function("getProgramOutputFilter", &getProgramOutputFilter);
68+
void getMessage_setData(rtbot::Message<rtbot::NumberData>& msg, const rtbot::NumberData& data) { msg.data = data; }
69+
} // namespace
6970

70-
emscripten::function("processBatch", &processBatch32);
71-
emscripten::function("processBatchDebug", &processBatch32Debug);
72-
}
71+
EMSCRIPTEN_BINDINGS(RtBot) {
72+
// Register NumberData type first
73+
value_object<rtbot::NumberData>("NumberData").field("value", &rtbot::NumberData::value);
74+
75+
// Register Message type with manual accessors to avoid base class issues
76+
class_<rtbot::Message<rtbot::NumberData>>("Message")
77+
.constructor<timestamp_t, const rtbot::NumberData&>()
78+
.property("time", &getMessage_getTime, &getMessage_setTime)
79+
.property("data", &getMessage_getData, &getMessage_setData);
80+
81+
// Core program management functions
82+
function("createProgram", &rtbot::create_program);
83+
function("deleteProgram", &rtbot::delete_program);
84+
function("validateProgram", &rtbot::validate_program);
85+
function("validateOperator", &rtbot::validate_operator);
86+
87+
// Message handling
88+
function("addToMessageBuffer", &addMessage);
89+
function("processMessageBuffer", &rtbot::process_message_buffer);
90+
function("processMessageBufferDebug", &rtbot::process_message_buffer_debug);
91+
92+
// Program information
93+
function("getProgramEntryOperatorId", &rtbot::get_program_entry_operator_id);
94+
function("getProgramEntryPorts", optional_override([](const std::string& programId) -> std::string {
95+
try {
96+
auto entry_id = rtbot::get_program_entry_operator_id(programId);
97+
if (entry_id.empty()) return "[]";
98+
99+
// By default, return ["i1"] as the entry port
100+
json ports = {"i1"};
101+
return ports.dump();
102+
} catch (const std::exception& e) {
103+
return "[]";
104+
}
105+
}));
106+
107+
// Batch processing
108+
function("processBatch", &processBatch32);
109+
function("processBatchDebug", &processBatch32Debug);
110+
111+
// State management
112+
// TODO: Serialize and deserialize program state
113+
}

libs/wrappers/javascript/src/api.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,19 @@ export class RtBotRun {
100100
// if (!success) throw new Error(`Program is invalid`);
101101
const program = this.program.toPlain();
102102
const programStr = JSON.stringify(program, null, 2);
103-
if (this.verbose) console.log("Sending", programStr);
104-
const createProgramResponseStr = await RtBot.getInstance().createProgram(this.program.programId, programStr);
105-
106-
if (createProgramResponseStr) {
107-
const createProgramResponse = JSON.parse(createProgramResponseStr);
108-
// if program fails validation, throw an error
109-
if (createProgramResponse.error) throw new Error(createProgramResponse.error);
103+
try {
104+
if (this.verbose) console.log("Sending", programStr);
105+
const createProgramResponseStr = await RtBot.getInstance().createProgram(this.program.programId, programStr);
106+
if (this.verbose) console.log("createProgram response", createProgramResponseStr);
107+
108+
if (createProgramResponseStr) {
109+
const createProgramResponse = JSON.parse(createProgramResponseStr);
110+
// if program fails validation, throw an error
111+
if (createProgramResponse.error) throw new Error(createProgramResponse.error);
112+
}
113+
} catch (e) {
114+
console.error("Error creating program", e);
115+
throw e;
110116
}
111117

112118
if (this.verbose) console.log("Sending data...");

libs/wrappers/javascript/src/program.spec.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,37 @@ const programJson = `{
1919
"operators": [
2020
{
2121
"id": "in1",
22-
"type": "Input"
22+
"type": "Input",
23+
"portTypes": ["number"]
2324
},
2425
{
2526
"id": "ma1",
2627
"type": "MovingAverage",
27-
"n": 6
28+
"window_size": 6
2829
},
2930
{
3031
"id": "ma2",
3132
"type": "MovingAverage",
32-
"n": 250
33+
"window_size": 250
3334
},
3435
{
3536
"id": "minus",
36-
"type": "Minus",
37-
"policies": { "i1": { "eager": false } }
37+
"type": "Subtraction"
3838
},
3939
{
4040
"id": "peak",
4141
"type": "PeakDetector",
42-
"n": 13
42+
"window_size": 13
4343
},
4444
{
4545
"id": "join",
4646
"type": "Join",
47-
"policies": { "i1": { "eager": false } },
48-
"numPorts": 2
47+
"portTypes": ["number", "number"]
4948
},
5049
{
5150
"id": "out1",
52-
"type": "Output"
51+
"type": "Output",
52+
"portTypes": ["number"]
5353
}
5454
],
5555
"connections": [
@@ -101,7 +101,10 @@ const programJson = `{
101101
"fromPort": "o1",
102102
"toPort": "i2"
103103
}
104-
]
104+
],
105+
"output": {
106+
"out1": ["o1"]
107+
}
105108
}
106109
`;
107110

@@ -112,24 +115,26 @@ describe("Program", () => {
112115

113116
beforeEach(() => {
114117
program = new Program("input1", title, description);
115-
const input = new Input("input1");
118+
const input = new Input("input1", ["number"]);
116119
const op1 = new MovingAverage("ma1", 2);
117-
const output = new Output("out1");
120+
const output = new Output("out1", ["number"]);
118121
program.addOperator(input);
119122
program.addOperator(op1);
120123
program.addOperator(output);
121124
program.addConnection(input, op1);
122125
program.addConnection(op1, output);
126+
program.addOutput("out1", ["o1"]);
123127
});
124128

129+
// Rest of the test cases remain the same...
125130
it("can create a new instance", () => {
126131
expect(program.title).toBe(title);
127132
});
128133

129134
it("can be serialized", () => {
130135
const json = JSON.stringify(program);
131136
const parsedProgram = JSON.parse(json);
132-
expect(parsedProgram.operators.find((op: any) => op.id === "ma1")).toMatchObject({ id: "ma1", n: 2 });
137+
expect(parsedProgram.operators.find((op: any) => op.id === "ma1")).toMatchObject({ id: "ma1", window_size: 2 });
133138
});
134139

135140
it("validates operator input", () => {

libs/wrappers/javascript/src/program.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class Program {
1111
connections: Connection[] = [];
1212
programId: string;
1313
defaultPort?: string;
14+
output: Record<string, string[]> = {};
1415

1516
constructor(
1617
public entryOperator?: string,
@@ -39,13 +40,35 @@ export class Program {
3940
}
4041

4142
validate() {
42-
programSchema.parse(JSON.parse(JSON.stringify(this)));
43+
try {
44+
programSchema.parse(JSON.parse(JSON.stringify(this)));
45+
} catch (error) {
46+
if (error instanceof Error) {
47+
// Extract operator info from error path if present
48+
const issues = JSON.parse(error.message);
49+
const operatorErrors = issues.filter((i: any) => i.path[0] === "operators");
50+
51+
if (operatorErrors.length > 0) {
52+
const operatorIndex = operatorErrors[0].path[1];
53+
const invalidOperator = this.operators[operatorIndex];
54+
throw new Error(
55+
`Invalid operator "${invalidOperator?.type}" at index ${operatorIndex}. ` +
56+
`Valid operators are: ${operatorErrors[0].unionErrors?.[1].issues[0].options.join(", ")}`
57+
);
58+
}
59+
}
60+
throw error;
61+
}
4362
}
4463

4564
safeValidate() {
4665
return programSchema.safeParse(JSON.parse(JSON.stringify(this)));
4766
}
4867

68+
addOutput(operatorId: string, ports: string[]) {
69+
this.output[operatorId] = ports;
70+
}
71+
4972
addOperator(op: Operator) {
5073
op.setProgram(this);
5174
this.operators.push(op);

tools/generator/src/index.ts

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -196,40 +196,67 @@ program
196196
}
197197

198198
if (target === "typescript") {
199+
const getTypeString = (prop: any): string => {
200+
if (prop.name === "outputMappings") {
201+
return "any";
202+
}
203+
if (prop.type === "array") {
204+
if (
205+
prop.items?.enum?.every((e: string) => ["number", "boolean", "vector_number", "vector_boolean"].includes(e))
206+
) {
207+
return "PortType[]";
208+
}
209+
if (prop.items?.enum) {
210+
const enumVals = prop.items.enum.map((e: string) => `"${e}"`).join(" | ");
211+
return `(${enumVals})[]`;
212+
}
213+
return "any[]";
214+
}
215+
if (prop.type === "string" && prop.enum) {
216+
if (prop.enum.every((e: string) => ["number", "boolean", "vector_number", "vector_boolean"].includes(e))) {
217+
return "PortType";
218+
}
219+
return prop.enum.map((e: string) => `"${e}"`).join(" | ");
220+
}
221+
const typeMap: Record<string, string> = {
222+
integer: "number",
223+
number: "number",
224+
string: "string",
225+
boolean: "boolean",
226+
object: "Record<string, any>",
227+
array: "any[]",
228+
};
229+
return typeMap[prop.type] || "any";
230+
};
231+
232+
const nonPrototypeSchemas = programJsonschema.properties.operators.items.oneOf.filter(
233+
(schema: any) => !schema.properties?.prototype
234+
);
235+
199236
const typescriptContent = typescriptTemplate({
200-
schemas: parseSchema({ type: "array", items: programJsonschema.properties.operators.items.oneOf })
201-
.replace("z.tuple(", "")
202-
.slice(0, -1),
203-
operators: await Promise.all(
204-
schemas.map(async (schema) => {
205-
const properties = Object.keys(schema.properties);
237+
schemas: nonPrototypeSchemas.map((schema) => parseSchema(schema)).join(",\n"),
238+
operators: schemas
239+
.filter((schema) => !schema.properties?.prototype)
240+
.map((schema) => {
206241
const type = schema.properties.type.enum[0];
207-
const ts = await compile(schema, type);
208-
let parametersBlock = ts
209-
.split("export interface")[1]
210-
.split("\n")
211-
.filter((l) => l.indexOf("type") === -1 && l.indexOf("unknown") === -1)
212-
.slice(1)
213-
.slice(0, -2)
214-
.map((l) => l.replace(";", ","))
242+
const props = Object.entries(schema.properties)
243+
.filter(([k]) => k !== "type")
244+
.map(([name, prop]: [string, any]) => {
245+
const description = prop.description ? `/**\n* ${prop.description}\n*/\n` : "";
246+
const typeStr = getTypeString({ ...prop, name });
247+
return `${description}readonly ${name}${schema.required?.includes(name) ? "" : "?"}: ${typeStr},`;
248+
})
215249
.join("\n");
216-
properties.forEach(
217-
(prop) =>
218-
(parametersBlock = parametersBlock
219-
.replace(`${prop}:`, `readonly ${prop}:`)
220-
.replace(`${prop}?:`, `readonly ${prop}?:`))
221-
);
222-
const zodSchema = parseSchema(schema);
223250

224251
return {
225252
type,
226-
parametersBlock,
227-
schema: zodSchema,
253+
parametersBlock: props,
254+
schema: parseSchema(schema),
228255
};
229-
})
230-
),
256+
}),
231257
});
232-
const formatted = await prettier.format(typescriptContent, { parser: "babel-ts" });
258+
259+
const formatted = typescriptContent; //await prettier.format(typescriptContent, { parser: "babel-ts" });
233260
fs.writeFileSync(`${output}/index.ts`, formatted);
234261
}
235262
});

0 commit comments

Comments
 (0)