@openrois/sdk 0.0.1-stage → 0.1.0-alpha.1

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,383 @@
1
+ /// <reference lib="esnext.disposable" />
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * A small typed event emitter for RoISClient and WebSocketTransport.
6
+ *
7
+ * Node's EventEmitter would need the `events` package in browsers, its types need
8
+ * @types/node, and it throws on an "error" event with no listener, so one bad message
9
+ * from a server would end the process. Here a fault is an event that nothing can turn
10
+ * into a throw: it goes to the "error" listeners, or to the console if there are none.
11
+ */
12
+ /** The events of an emitter: each name maps to its listener's arguments. */
13
+ type EmitterEvents<Events> = Record<keyof Events, unknown[]> & {
14
+ error: [error: Error];
15
+ };
16
+ /** Base class of whatever emits events. Only the class itself can emit. */
17
+ declare class TypedEmitter<Events extends EmitterEvents<Events>> {
18
+ private readonly listeners;
19
+ /** Add a listener. It stays until off() removes it. */
20
+ on<E extends keyof Events>(event: E, listener: (...args: Events[E]) => void): this;
21
+ /** Remove a listener added with on(). */
22
+ off<E extends keyof Events>(event: E, listener: (...args: Events[E]) => void): this;
23
+ /**
24
+ * Call the listeners of an event. A listener that throws, or an async listener that
25
+ * rejects, is reported as a fault, because the caller is the reader of a socket and
26
+ * must carry on with the next message.
27
+ */
28
+ protected emit<E extends keyof Events>(event: E, ...args: Events[E]): void;
29
+ /** Pass on what a listener threw. An "error" listener that throws can only be logged. */
30
+ private reportFault;
31
+ }
32
+
33
+ /**
34
+ * JSON-RPC 2.0 envelopes for the OpenRoIS SDK.
35
+ *
36
+ * Defines the four message types exchanged with an engine over WebSocket:
37
+ *
38
+ * JsonRpcRequest - A call that expects a reply (has `id`)
39
+ * JsonRpcResponse - A successful reply to a request
40
+ * JsonRpcError - A failed reply to a request
41
+ * JsonRpcNotification - A push with no reply expected (no `id`)
42
+ *
43
+ * This module knows nothing about RoIS. The method names, the params and result
44
+ * of each method, the notifications and the JSON-RPC error codes come from the
45
+ * method catalog in @openrois/interfaces (RoISMethods, RoISMethodSchemas,
46
+ * RoISNotifications, JsonRpcErrorCode). Section 17 of docs/rois-reference.md
47
+ * describes the binding.
48
+ *
49
+ * A RoIS failure is a normal JsonRpcResponse whose result has a return_code
50
+ * other than OK. A JsonRpcError only reports a protocol fault: unparseable
51
+ * JSON, an invalid request, an unknown method, params that fail validation, or
52
+ * an internal error.
53
+ *
54
+ * Source: JSON-RPC 2.0 Specification (https://www.jsonrpc.org/specification)
55
+ */
56
+
57
+ /**
58
+ * The error object carried inside a JsonRpcError message.
59
+ *
60
+ * Maps to the Error Object defined in section 5.1 of the JSON-RPC 2.0 spec.
61
+ * The standard codes are in JsonRpcErrorCode. Codes from -32000 to -32099 are
62
+ * left to the engine.
63
+ */
64
+ declare const JsonRpcErrorObjectSchema: z.ZodObject<{
65
+ code: z.ZodNumber;
66
+ message: z.ZodString;
67
+ data: z.ZodOptional<z.ZodUnknown>;
68
+ }, z.core.$strict>;
69
+ type JsonRpcErrorObject = z.infer<typeof JsonRpcErrorObjectSchema>;
70
+ /**
71
+ * A JSON-RPC 2.0 Notification: a push from the engine with no `id`, so the
72
+ * client never replies. The `params` are the catalog's params model for the
73
+ * notification.
74
+ *
75
+ * Example:
76
+ * {
77
+ * "jsonrpc": "2.0",
78
+ * "method": "rois.command.completed",
79
+ * "params": { "command_id": "3f2a9c1e-5b7d-4e2a-9f10-6c8d2b4a7e01", "status": "OK" }
80
+ * }
81
+ */
82
+ declare const JsonRpcNotificationSchema: z.ZodObject<{
83
+ jsonrpc: z.ZodLiteral<"2.0">;
84
+ method: z.ZodString;
85
+ params: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodArray<z.ZodUnknown>]>>;
86
+ }, z.core.$strict>;
87
+ type JsonRpcNotification = z.infer<typeof JsonRpcNotificationSchema>;
88
+
89
+ /**
90
+ * WebSocket transport layer for the OpenRoIS SDK.
91
+ *
92
+ * Carries the JSON-RPC messages between RoISClient and an engine.
93
+ * Responsibilities:
94
+ *
95
+ * - Establish and manage a WebSocket connection (browser or Node.js).
96
+ * - Serialize outgoing JSON-RPC requests and deserialize incoming messages.
97
+ * - Correlate responses to pending requests by matching the `id` field.
98
+ * - Route server-push notifications to listeners.
99
+ * - Handle connection errors, timeouts, and unexpected disconnects.
100
+ *
101
+ * The transport does NOT validate the inner payload of `params` or `result`.
102
+ * RoISClient does that with the catalog schemas from @openrois/interfaces
103
+ * (RoISMethodSchemas and RoISNotificationSchemas).
104
+ *
105
+ * Usage (both Node.js and Browser):
106
+ * const transport = new WebSocketTransport();
107
+ * await transport.connect("ws://localhost:8765");
108
+ * const result = await transport.send("rois.command.search", { condition: "" });
109
+ * transport.close();
110
+ *
111
+ * The transport uses the global WebSocket, which browsers and Node.js 22 both
112
+ * have. TransportOptions.webSocketFactory replaces it, for example to pass
113
+ * headers with the `ws` package.
114
+ *
115
+ * Architecture: docs/architecture.md section 5 (Client & Service Application SDK)
116
+ */
117
+
118
+ /**
119
+ * Minimal WebSocket interface satisfied by the global WebSocket of browsers and
120
+ * Node.js, and by the `ws` package.
121
+ *
122
+ * We use the property-based event handler style (onopen, onmessage, etc.)
123
+ * because all of them support it identically. The transport assigns these
124
+ * handlers after creating the socket.
125
+ */
126
+ interface WebSocketLike {
127
+ send(data: string): void;
128
+ close(code?: number, reason?: string): void;
129
+ readonly readyState: number;
130
+ onopen: ((event: {
131
+ type: string;
132
+ }) => void) | null;
133
+ onclose: ((event: {
134
+ code: number;
135
+ reason: string;
136
+ type: string;
137
+ }) => void) | null;
138
+ onmessage: ((event: {
139
+ data: unknown;
140
+ type: string;
141
+ }) => void) | null;
142
+ onerror: ((event: {
143
+ error?: unknown;
144
+ message?: string;
145
+ type: string;
146
+ }) => void) | null;
147
+ }
148
+ /**
149
+ * Connection lifecycle states for the transport.
150
+ *
151
+ * State transitions:
152
+ * DISCONNECTED -> CONNECTING -> CONNECTED -> CLOSING -> DISCONNECTED
153
+ * -> DISCONNECTED (unexpected close)
154
+ */
155
+ declare enum TransportState {
156
+ /** No active connection. Initial state and state after close. */
157
+ DISCONNECTED = "DISCONNECTED",
158
+ /** connect() has been called, and the WebSocket has not opened yet. */
159
+ CONNECTING = "CONNECTING",
160
+ /** WebSocket is open and ready to send/receive messages. */
161
+ CONNECTED = "CONNECTED",
162
+ /** close() has been called, and the WebSocket has not finished closing yet. */
163
+ CLOSING = "CLOSING"
164
+ }
165
+ /**
166
+ * Options for configuring the WebSocket transport.
167
+ */
168
+ interface TransportOptions {
169
+ /**
170
+ * Maximum time in milliseconds to wait for a response to a request.
171
+ * If the gateway does not reply within this window, the Promise returned
172
+ * by send() rejects with a RequestTimeoutError. Default: 30000 (30s).
173
+ */
174
+ requestTimeout?: number;
175
+ /**
176
+ * Maximum time in milliseconds to wait for the WebSocket connection to
177
+ * open during connect(). Default: 10000 (10s).
178
+ */
179
+ connectTimeout?: number;
180
+ /**
181
+ * Optional factory function that creates a WebSocket instance for a given URL.
182
+ *
183
+ * By default, the transport uses the global WebSocket. Override this if you
184
+ * need custom WebSocket configuration (headers, protocols, proxy settings,
185
+ * etc.) or run somewhere that has no global WebSocket.
186
+ *
187
+ * Example (Node.js, `ws` package):
188
+ * webSocketFactory: (url) =>
189
+ * new WebSocket(url, { perMessageDeflate: false }) as unknown as WebSocketLike
190
+ */
191
+ webSocketFactory?: (url: string) => WebSocketLike;
192
+ }
193
+ /**
194
+ * Base error for all transport-level failures.
195
+ *
196
+ * Callers can catch TransportError to handle any transport problem, or catch
197
+ * a specific subclass for finer control.
198
+ */
199
+ declare class TransportError extends Error {
200
+ constructor(message: string);
201
+ }
202
+ /**
203
+ * Raised when the WebSocket connection cannot be established or is lost
204
+ * unexpectedly.
205
+ */
206
+ declare class ConnectionError extends TransportError {
207
+ /** The URL that the transport tried to connect to, if available. */
208
+ readonly url: string;
209
+ constructor(message: string, url?: string);
210
+ }
211
+ /**
212
+ * Raised when a send() call does not receive a response within the
213
+ * configured timeout window.
214
+ */
215
+ declare class RequestTimeoutError extends TransportError {
216
+ /** The JSON-RPC method that timed out. */
217
+ readonly method: string;
218
+ /** The request id that timed out. */
219
+ readonly requestId: string;
220
+ constructor(method: string, requestId: string, timeoutMs: number);
221
+ }
222
+ /**
223
+ * Raised when the engine responds with a JSON-RPC error object.
224
+ *
225
+ * This reports a protocol fault: unparseable JSON, an invalid request, an
226
+ * unknown method, params that fail validation, or an internal error. A RoIS
227
+ * failure is not an RpcError: it is a normal result with a return_code other
228
+ * than OK, which RoISClient raises as a RoISError.
229
+ */
230
+ declare class RpcError extends TransportError {
231
+ /** The numeric JSON-RPC error code (e.g. -32601 for MethodNotFound). */
232
+ readonly code: number;
233
+ /** Optional additional error data from the gateway. */
234
+ readonly data: unknown;
235
+ constructor(errorObject: JsonRpcErrorObject);
236
+ }
237
+ /**
238
+ * The events of WebSocketTransport, with their listener arguments.
239
+ *
240
+ * "open" - The WebSocket connection opened successfully.
241
+ * "notification" - A JSON-RPC notification arrived from the gateway, with any
242
+ * method, in or outside the catalog.
243
+ * "close" - The WebSocket connection closed.
244
+ * "error" - A message from the gateway could not be used.
245
+ */
246
+ type TransportEvents = {
247
+ open: [];
248
+ notification: [notification: JsonRpcNotification];
249
+ close: [code: number, reason: string];
250
+ error: [error: Error];
251
+ };
252
+ /**
253
+ * Manages a single WebSocket connection to the OpenRoIS gateway.
254
+ *
255
+ * The transport handles the JSON-RPC 2.0 wire protocol: serialization,
256
+ * deserialization, request/response correlation, notification routing,
257
+ * and error handling. RoISClient calls transport.send() and listens to the
258
+ * transport events.
259
+ *
260
+ * One transport instance manages one connection. To reconnect, close the
261
+ * existing connection and call connect() again.
262
+ */
263
+ declare class WebSocketTransport extends TypedEmitter<TransportEvents> {
264
+ /** Current connection state. */
265
+ private _state;
266
+ /** The underlying WebSocket instance, or null when disconnected. */
267
+ private ws;
268
+ /** Map of request id -> pending request awaiting a response. */
269
+ private pending;
270
+ /** Counter for generating unique request ids within this connection. */
271
+ private nextId;
272
+ /** Resolved configuration with defaults applied. */
273
+ private options;
274
+ /** The URL this transport is connected (or connecting) to. */
275
+ private url;
276
+ /** Ends the connect() that is in progress, or null when none is. */
277
+ private abortConnect;
278
+ constructor(options?: TransportOptions);
279
+ /** The current connection state. */
280
+ get state(): TransportState;
281
+ /**
282
+ * Open a WebSocket connection to the gateway.
283
+ *
284
+ * Resolves when the connection is established and ready for messages.
285
+ * Rejects with ConnectionError if the connection fails or times out, or if
286
+ * close() runs first.
287
+ *
288
+ * @param url - The WebSocket URL, e.g. "ws://localhost:8765" or
289
+ * "wss://gateway.example.com".
290
+ */
291
+ connect(url: string): Promise<void>;
292
+ /**
293
+ * Send a JSON-RPC request to the gateway and wait for the response.
294
+ *
295
+ * Returns a Promise that resolves with the `result` field from the
296
+ * success response, or rejects with:
297
+ * - RpcError: the gateway sent a JSON-RPC error response.
298
+ * - RequestTimeoutError: no response within the timeout window.
299
+ * - ConnectionError: the connection dropped while waiting.
300
+ * - TransportError: the transport is not connected.
301
+ *
302
+ * The caller is responsible for validating the returned result with the
303
+ * catalog schema for the method:
304
+ * const result = await transport.send("rois.command.search", { condition: "" });
305
+ * const parsed = RoISMethodSchemas["rois.command.search"].result.parse(result);
306
+ *
307
+ * @param method - The JSON-RPC method name, e.g. "rois.command.search".
308
+ * @param params - Optional named parameters for the method.
309
+ * @returns The `result` field from the gateway's success response.
310
+ */
311
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>;
312
+ /**
313
+ * Close the WebSocket connection.
314
+ *
315
+ * Any pending requests are rejected with a ConnectionError.
316
+ * Safe to call when already disconnected (no-op).
317
+ *
318
+ * During connect(), this ends the attempt instead: connect() rejects with a
319
+ * ConnectionError and no "close" event fires, because no connection opened.
320
+ *
321
+ * @param code - WebSocket close code (default: 1000 for normal closure): 1000, or
322
+ * 3000 to 4999, which is what the WebSocket API lets a client send.
323
+ * @param reason - Human-readable close reason, at most 123 bytes of UTF-8.
324
+ * @throws TransportError for any other code or reason, in every state, and a
325
+ * connection stays open.
326
+ */
327
+ close(code?: number, reason?: string): void;
328
+ /**
329
+ * Process a raw message string from the WebSocket.
330
+ *
331
+ * Parses the JSON, classifies the message type, and dispatches to the
332
+ * appropriate handler (response correlation or notification routing).
333
+ */
334
+ private handleMessage;
335
+ /**
336
+ * Handle a success response by resolving the matching pending request.
337
+ */
338
+ private handleResponse;
339
+ /**
340
+ * Handle an error response by rejecting the matching pending request.
341
+ */
342
+ private handleErrorResponse;
343
+ /**
344
+ * Handle a notification by emitting it as an event. It goes out only as
345
+ * "notification", never under its method name, because a server could then
346
+ * fake the "close" event by naming a notification after it.
347
+ */
348
+ private handleNotification;
349
+ /**
350
+ * Handle WebSocket close after the connection was established.
351
+ *
352
+ * If the close was not initiated by the client (i.e. the state is still
353
+ * CONNECTED), this is an unexpected disconnect. All pending requests are
354
+ * rejected.
355
+ */
356
+ private handleClose;
357
+ /**
358
+ * Reject all pending requests with the given error.
359
+ *
360
+ * Called on disconnect (expected or unexpected) so no Promise is left
361
+ * hanging forever.
362
+ */
363
+ private rejectAllPending;
364
+ /**
365
+ * Null out the WebSocket reference and remove event handlers.
366
+ *
367
+ * Prevents stale handlers from firing after the transport considers
368
+ * itself disconnected. The error handler stays as a no-op, because the `ws`
369
+ * package throws a late socket error that has no handler, and it raises one
370
+ * when a socket closes before its handshake ends.
371
+ */
372
+ private cleanupSocket;
373
+ /**
374
+ * Generate a unique request id.
375
+ *
376
+ * Uses a simple incrementing counter prefixed with "req-". Ids are unique
377
+ * within a single transport instance. The counter resets if you create a
378
+ * new transport.
379
+ */
380
+ private generateId;
381
+ }
382
+
383
+ export { ConnectionError as C, RequestTimeoutError as R, type TransportOptions as T, WebSocketTransport as W, TypedEmitter as a, RpcError as b, TransportError as c, type TransportEvents as d, TransportState as e, type WebSocketLike as f };
@@ -0,0 +1,3 @@
1
+ /// <reference lib="esnext.disposable" />
2
+ export { C as ConnectionError, R as RequestTimeoutError, b as RpcError, c as TransportError, d as TransportEvents, T as TransportOptions, e as TransportState, f as WebSocketLike, W as WebSocketTransport } from './transport-kMEIcw5y.js';
3
+ import 'zod';
@@ -0,0 +1,17 @@
1
+ import {
2
+ ConnectionError,
3
+ RequestTimeoutError,
4
+ RpcError,
5
+ TransportError,
6
+ TransportState,
7
+ WebSocketTransport
8
+ } from "./chunk-AEO3U7Y7.js";
9
+ export {
10
+ ConnectionError,
11
+ RequestTimeoutError,
12
+ RpcError,
13
+ TransportError,
14
+ TransportState,
15
+ WebSocketTransport
16
+ };
17
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,13 +1,81 @@
1
1
  {
2
2
  "name": "@openrois/sdk",
3
- "version": "0.0.1-stage",
4
- "description": "Placeholder for the OpenRoIS client SDK",
5
- "main": "index.js",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "JavaScript client SDK for the OpenRoIS middleware",
5
+ "license": "Apache-2.0",
6
+ "author": "OpenRoIS Community",
7
+ "type": "module",
8
+ "main": "./dist/rois-client.js",
9
+ "types": "./dist/rois-client.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/rois-client.d.ts",
13
+ "default": "./dist/rois-client.js"
14
+ },
15
+ "./transport": {
16
+ "types": "./dist/transport.d.ts",
17
+ "default": "./dist/transport.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "NOTICE"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.12"
29
+ },
30
+ "sideEffects": false,
31
+ "directories": {
32
+ "test": "tests"
33
+ },
6
34
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
35
+ "build": "tsup",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "test:browser": "vitest run --config vitest.browser.config.ts",
39
+ "test:consumer": "npm run build && node tests/consumer/run.mjs",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "eslint src/ tests/",
42
+ "prepublishOnly": "npm run build"
43
+ },
44
+ "dependencies": {
45
+ "@openrois/interfaces": "^0.1.0-alpha.6",
46
+ "zod": "^4.4.3"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^26.1.0",
50
+ "@types/ws": "^8.18.1",
51
+ "@vitest/browser-playwright": "^4.1.9",
52
+ "eslint": "^10.5.0",
53
+ "jiti": "^2.7.0",
54
+ "playwright": "1.58.2",
55
+ "tsup": "^8.5.1",
56
+ "typescript": "^6.0.3",
57
+ "typescript-eslint": "^8.62.1",
58
+ "vite": "^8.1.0",
59
+ "vitest": "^4.1.9",
60
+ "ws": "^8.21.0"
61
+ },
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "https://github.com/openrois/openrois",
65
+ "directory": "sdk/javascript"
66
+ },
67
+ "homepage": "https://openrois.org/",
68
+ "bugs": {
69
+ "url": "https://github.com/openrois/openrois/issues"
8
70
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC",
12
- "type": "commonjs"
71
+ "keywords": [
72
+ "rois",
73
+ "omg",
74
+ "robotics",
75
+ "hri",
76
+ "middleware",
77
+ "sdk",
78
+ "json-rpc",
79
+ "websocket"
80
+ ]
13
81
  }