rocketride 1.0.6 → 1.1.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/README.md +2 -2
- package/dist/cjs/account.js +284 -0
- package/dist/cjs/account.js.map +1 -0
- package/dist/cjs/billing.js +171 -0
- package/dist/cjs/billing.js.map +1 -0
- package/dist/cjs/client.js +1208 -756
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/constants.js +10 -1
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/core/DAPBase.js +4 -1
- package/dist/cjs/core/DAPBase.js.map +1 -1
- package/dist/cjs/core/DAPClient.js +121 -50
- package/dist/cjs/core/DAPClient.js.map +1 -1
- package/dist/cjs/core/TransportBase.js +0 -10
- package/dist/cjs/core/TransportBase.js.map +1 -1
- package/dist/cjs/core/TransportWebSocket.js +30 -19
- package/dist/cjs/core/TransportWebSocket.js.map +1 -1
- package/dist/cjs/database.js +121 -0
- package/dist/cjs/database.js.map +1 -0
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/schema/Question.js +2 -0
- package/dist/cjs/schema/Question.js.map +1 -1
- package/dist/cjs/types/account.js +26 -0
- package/dist/cjs/types/account.js.map +1 -0
- package/dist/cjs/types/billing.js +26 -0
- package/dist/cjs/types/billing.js.map +1 -0
- package/dist/cjs/types/client.js +14 -0
- package/dist/cjs/types/client.js.map +1 -1
- package/dist/cjs/types/cprofile.js +26 -0
- package/dist/cjs/types/cprofile.js.map +1 -0
- package/dist/cjs/types/dashboard.js +26 -0
- package/dist/cjs/types/dashboard.js.map +1 -0
- package/dist/cjs/types/events.js +5 -1
- package/dist/cjs/types/events.js.map +1 -1
- package/dist/cjs/types/index.js +5 -0
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/service.js +85 -0
- package/dist/cjs/types/service.js.map +1 -0
- package/dist/cli/cli/rocketride.js +335 -113
- package/dist/cli/cli/rocketride.js.map +1 -1
- package/dist/cli/client/account.js +284 -0
- package/dist/cli/client/account.js.map +1 -0
- package/dist/cli/client/billing.js +171 -0
- package/dist/cli/client/billing.js.map +1 -0
- package/dist/cli/client/client.js +1208 -756
- package/dist/cli/client/client.js.map +1 -1
- package/dist/cli/client/constants.js +10 -1
- package/dist/cli/client/constants.js.map +1 -1
- package/dist/cli/client/core/DAPBase.js +4 -1
- package/dist/cli/client/core/DAPBase.js.map +1 -1
- package/dist/cli/client/core/DAPClient.js +121 -50
- package/dist/cli/client/core/DAPClient.js.map +1 -1
- package/dist/cli/client/core/TransportBase.js +0 -10
- package/dist/cli/client/core/TransportBase.js.map +1 -1
- package/dist/cli/client/core/TransportWebSocket.js +30 -19
- package/dist/cli/client/core/TransportWebSocket.js.map +1 -1
- package/dist/cli/client/database.js +121 -0
- package/dist/cli/client/database.js.map +1 -0
- package/dist/cli/client/index.js +4 -0
- package/dist/cli/client/index.js.map +1 -1
- package/dist/cli/client/schema/Question.js +2 -0
- package/dist/cli/client/schema/Question.js.map +1 -1
- package/dist/cli/client/types/account.js +26 -0
- package/dist/cli/client/types/account.js.map +1 -0
- package/dist/cli/client/types/billing.js +26 -0
- package/dist/cli/client/types/billing.js.map +1 -0
- package/dist/cli/client/types/client.js +14 -0
- package/dist/cli/client/types/client.js.map +1 -1
- package/dist/cli/client/types/cprofile.js +26 -0
- package/dist/cli/client/types/cprofile.js.map +1 -0
- package/dist/cli/client/types/dashboard.js +26 -0
- package/dist/cli/client/types/dashboard.js.map +1 -0
- package/dist/cli/client/types/events.js +5 -1
- package/dist/cli/client/types/events.js.map +1 -1
- package/dist/cli/client/types/index.js +5 -0
- package/dist/cli/client/types/index.js.map +1 -1
- package/dist/cli/client/types/service.js +85 -0
- package/dist/cli/client/types/service.js.map +1 -0
- package/dist/esm/account.js +280 -0
- package/dist/esm/account.js.map +1 -0
- package/dist/esm/billing.js +167 -0
- package/dist/esm/billing.js.map +1 -0
- package/dist/esm/client.js +1208 -756
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/constants.js +9 -0
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/core/DAPBase.js +4 -1
- package/dist/esm/core/DAPBase.js.map +1 -1
- package/dist/esm/core/DAPClient.js +121 -50
- package/dist/esm/core/DAPClient.js.map +1 -1
- package/dist/esm/core/TransportBase.js +0 -10
- package/dist/esm/core/TransportBase.js.map +1 -1
- package/dist/esm/core/TransportWebSocket.js +30 -19
- package/dist/esm/core/TransportWebSocket.js.map +1 -1
- package/dist/esm/database.js +117 -0
- package/dist/esm/database.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/schema/Question.js +2 -0
- package/dist/esm/schema/Question.js.map +1 -1
- package/dist/esm/types/account.js +25 -0
- package/dist/esm/types/account.js.map +1 -0
- package/dist/esm/types/billing.js +25 -0
- package/dist/esm/types/billing.js.map +1 -0
- package/dist/esm/types/client.js +13 -1
- package/dist/esm/types/client.js.map +1 -1
- package/dist/esm/types/cprofile.js +25 -0
- package/dist/esm/types/cprofile.js.map +1 -0
- package/dist/esm/types/dashboard.js +25 -0
- package/dist/esm/types/dashboard.js.map +1 -0
- package/dist/esm/types/events.js +5 -1
- package/dist/esm/types/events.js.map +1 -1
- package/dist/esm/types/index.js +5 -0
- package/dist/esm/types/index.js.map +1 -1
- package/dist/esm/types/service.js +82 -0
- package/dist/esm/types/service.js.map +1 -0
- package/dist/types/account.d.ts +209 -0
- package/dist/types/account.d.ts.map +1 -0
- package/dist/types/billing.d.ts +135 -0
- package/dist/types/billing.d.ts.map +1 -0
- package/dist/types/client.d.ts +553 -285
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/constants.d.ts +9 -0
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/core/DAPBase.d.ts.map +1 -1
- package/dist/types/core/DAPClient.d.ts +89 -7
- package/dist/types/core/DAPClient.d.ts.map +1 -1
- package/dist/types/core/TransportBase.d.ts +1 -11
- package/dist/types/core/TransportBase.d.ts.map +1 -1
- package/dist/types/core/TransportWebSocket.d.ts +14 -11
- package/dist/types/core/TransportWebSocket.d.ts.map +1 -1
- package/dist/types/database.d.ts +90 -0
- package/dist/types/database.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/schema/Question.d.ts +3 -1
- package/dist/types/schema/Question.d.ts.map +1 -1
- package/dist/types/types/account.d.ts +171 -0
- package/dist/types/types/account.d.ts.map +1 -0
- package/dist/types/types/billing.d.ts +115 -0
- package/dist/types/types/billing.d.ts.map +1 -0
- package/dist/types/types/client.d.ts +241 -1
- package/dist/types/types/client.d.ts.map +1 -1
- package/dist/types/types/cprofile.d.ts +67 -0
- package/dist/types/types/cprofile.d.ts.map +1 -0
- package/dist/types/types/dashboard.d.ts +198 -0
- package/dist/types/types/dashboard.d.ts.map +1 -0
- package/dist/types/types/events.d.ts +90 -95
- package/dist/types/types/events.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +5 -0
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/types/pipeline.d.ts +10 -2
- package/dist/types/types/pipeline.d.ts.map +1 -1
- package/dist/types/types/service.d.ts +189 -0
- package/dist/types/types/service.d.ts.map +1 -0
- package/dist/types/types/task.d.ts +5 -1
- package/dist/types/types/task.d.ts.map +1 -1
- package/package.json +12 -7
package/dist/esm/client.js
CHANGED
|
@@ -23,8 +23,12 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { TransportWebSocket } from './core/TransportWebSocket.js';
|
|
25
25
|
import { DAPClient } from './core/DAPClient.js';
|
|
26
|
+
import { TraceType } from './types/index.js';
|
|
26
27
|
import { CONST_DEFAULT_WEB_CLOUD, CONST_DEFAULT_WEB_PROTOCOL, CONST_DEFAULT_WEB_PORT } from './constants.js';
|
|
27
|
-
import {
|
|
28
|
+
import { AccountApi } from './account.js';
|
|
29
|
+
import { BillingApi } from './billing.js';
|
|
30
|
+
import { DatabaseApi } from './database.js';
|
|
31
|
+
import { AuthenticationException, ConnectionException, PipeException } from './exceptions/index.js';
|
|
28
32
|
// Global counter for generating unique client IDs
|
|
29
33
|
let clientId = 0;
|
|
30
34
|
/**
|
|
@@ -97,7 +101,8 @@ export class DataPipe {
|
|
|
97
101
|
* unique pipe ID that is used for subsequent operations.
|
|
98
102
|
*
|
|
99
103
|
* @returns This DataPipe instance (for method chaining)
|
|
100
|
-
* @throws Error if the pipe is already opened
|
|
104
|
+
* @throws Error if the pipe is already opened
|
|
105
|
+
* @throws PipeException if the server rejects the open request
|
|
101
106
|
*/
|
|
102
107
|
async open() {
|
|
103
108
|
if (this._opened) {
|
|
@@ -114,15 +119,32 @@ export class DataPipe {
|
|
|
114
119
|
});
|
|
115
120
|
const response = await this._client.request(request);
|
|
116
121
|
if (this._client.didFail(response)) {
|
|
117
|
-
|
|
122
|
+
const base = response.message || 'Failed to open a data pipe.';
|
|
123
|
+
const msg = `${base}\n\n` + 'Common causes:\n' + "- Pipeline isn't running (wrong token or task terminated)\n" + "- Pipeline source is 'chat' (use client.chat()), not webhook/dropper\n" + "- MIME type doesn't match the source lane (try mimeType='text/plain')\n";
|
|
124
|
+
throw new PipeException({ ...response, message: msg });
|
|
118
125
|
}
|
|
119
126
|
this._pipeId = response.body?.pipe_id;
|
|
120
|
-
this._opened = true;
|
|
121
127
|
// If an SSE callback was provided, subscribe and register for this pipe
|
|
122
128
|
if (this._onSSE !== undefined && this._pipeId !== undefined) {
|
|
123
|
-
|
|
129
|
+
try {
|
|
130
|
+
await this._client.setEvents(this._token, ['SSE'], this._pipeId);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
// Roll back: don't leave the pipe half-open on the server.
|
|
134
|
+
try {
|
|
135
|
+
await this.close();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Best-effort cleanup
|
|
139
|
+
}
|
|
140
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
141
|
+
const msg = `Failed to subscribe to SSE events for this data pipe.\n\n${errMsg}`;
|
|
142
|
+
throw new PipeException({ message: msg });
|
|
143
|
+
}
|
|
124
144
|
this._client._ssePipeCallbacks.set(this._pipeId, this._onSSE);
|
|
125
145
|
}
|
|
146
|
+
// Only mark opened after the server-side pipe is fully consistent (including SSE setup).
|
|
147
|
+
this._opened = true;
|
|
126
148
|
return this;
|
|
127
149
|
}
|
|
128
150
|
/**
|
|
@@ -132,7 +154,8 @@ export class DataPipe {
|
|
|
132
154
|
* multiple times to stream large datasets. The pipe must be opened first.
|
|
133
155
|
*
|
|
134
156
|
* @param buffer - Data to write, must be a Uint8Array
|
|
135
|
-
* @throws Error if the pipe is not opened
|
|
157
|
+
* @throws Error if the pipe is not opened or buffer is invalid
|
|
158
|
+
* @throws PipeException if the server reports a write failure
|
|
136
159
|
*/
|
|
137
160
|
async write(buffer) {
|
|
138
161
|
if (!this._opened) {
|
|
@@ -151,7 +174,8 @@ export class DataPipe {
|
|
|
151
174
|
});
|
|
152
175
|
const response = await this._client.request(request);
|
|
153
176
|
if (this._client.didFail(response)) {
|
|
154
|
-
|
|
177
|
+
const msg = response.message || 'Failed to write to a data pipe.';
|
|
178
|
+
throw new PipeException({ ...response, message: msg });
|
|
155
179
|
}
|
|
156
180
|
}
|
|
157
181
|
/**
|
|
@@ -162,10 +186,12 @@ export class DataPipe {
|
|
|
162
186
|
* the pipe cannot be reopened or written to again.
|
|
163
187
|
*
|
|
164
188
|
* @returns The processing result from the server, or undefined if already closed
|
|
165
|
-
* @throws
|
|
189
|
+
* @throws PipeException if the server reports a failure while finalizing the pipe
|
|
166
190
|
*/
|
|
167
191
|
async close() {
|
|
168
|
-
|
|
192
|
+
// Allow closing after a failed open() path where the server assigned a pipe_id
|
|
193
|
+
// but we never flipped _opened=true (e.g., SSE subscription failure).
|
|
194
|
+
if (this._closed || (this._pipeId === undefined && !this._opened)) {
|
|
169
195
|
return;
|
|
170
196
|
}
|
|
171
197
|
try {
|
|
@@ -178,12 +204,14 @@ export class DataPipe {
|
|
|
178
204
|
});
|
|
179
205
|
const response = await this._client.request(request);
|
|
180
206
|
if (this._client.didFail(response)) {
|
|
181
|
-
|
|
207
|
+
const msg = response.message || 'Failed to close a data pipe.';
|
|
208
|
+
throw new PipeException({ ...response, message: msg });
|
|
182
209
|
}
|
|
183
210
|
return response.body;
|
|
184
211
|
}
|
|
185
212
|
finally {
|
|
186
213
|
this._closed = true;
|
|
214
|
+
this._opened = false;
|
|
187
215
|
// Unregister SSE callback and scoped monitor subscription
|
|
188
216
|
if (this._onSSE !== undefined && this._pipeId !== undefined) {
|
|
189
217
|
this._client._ssePipeCallbacks.delete(this._pipeId);
|
|
@@ -197,24 +225,6 @@ export class DataPipe {
|
|
|
197
225
|
}
|
|
198
226
|
}
|
|
199
227
|
}
|
|
200
|
-
/**
|
|
201
|
-
* Main RocketRide client for connecting to RocketRide servers and services.
|
|
202
|
-
*
|
|
203
|
-
* This client provides a comprehensive API for interacting with RocketRide services,
|
|
204
|
-
* including connection management, pipeline execution, data operations, AI chat,
|
|
205
|
-
* event handling, and server connectivity testing.
|
|
206
|
-
*
|
|
207
|
-
* Key features:
|
|
208
|
-
* - Single shared WebSocket connection for all operations
|
|
209
|
-
* - Connection management (connect/disconnect) with optional persistence
|
|
210
|
-
* - Automatic reconnection when persist mode is enabled
|
|
211
|
-
* - Pipeline execution (use, terminate, getTaskStatus)
|
|
212
|
-
* - Data operations (send, sendFiles, pipe)
|
|
213
|
-
* - AI chat functionality (chat)
|
|
214
|
-
* - Event handling (setEvents, event callbacks)
|
|
215
|
-
* - Server connectivity testing (ping)
|
|
216
|
-
* - Full TypeScript type safety
|
|
217
|
-
*/
|
|
218
228
|
export class RocketRideClient extends DAPClient {
|
|
219
229
|
/**
|
|
220
230
|
* Creates a new RocketRideClient instance.
|
|
@@ -277,24 +287,26 @@ export class RocketRideClient extends DAPClient {
|
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
289
|
}
|
|
280
|
-
const { auth = config.auth
|
|
290
|
+
const { auth = config.auth, uri = config.uri || clientEnv.ROCKETRIDE_URI || CONST_DEFAULT_WEB_CLOUD, onEvent, onConnected, onDisconnected, onConnectError, persist, module } = config;
|
|
281
291
|
// Create unique client identifier
|
|
282
292
|
const clientName = module || `CLIENT-${clientId++}`;
|
|
283
|
-
// Initialize the DAPClient without a transport; transport is created in
|
|
293
|
+
// Initialize the DAPClient without a transport; transport is created in _internalAttach
|
|
284
294
|
super(clientName, undefined, config);
|
|
285
295
|
this._dapAttempted = false;
|
|
286
296
|
this._nextChatId = 1;
|
|
287
297
|
/** Maps pipe_id → SSE callback for pipe-scoped real-time event dispatch. */
|
|
288
298
|
this._ssePipeCallbacks = new Map();
|
|
289
|
-
//
|
|
299
|
+
// Desired state model — replaces old flag soup
|
|
300
|
+
this._desiredState = 'detached';
|
|
301
|
+
this._authenticated = false;
|
|
290
302
|
this._persist = false;
|
|
291
|
-
this._manualDisconnect = false;
|
|
292
303
|
this._currentReconnectDelay = 250;
|
|
293
|
-
/**
|
|
294
|
-
this.
|
|
304
|
+
/** Reference-counted monitor subscriptions: keyString → Map<eventType, refCount> */
|
|
305
|
+
this._monitorKeys = new Map();
|
|
295
306
|
// Store connection details and environment
|
|
307
|
+
this._wsPath = config.wsPath;
|
|
296
308
|
this._setUri(uri);
|
|
297
|
-
this._setAuth(auth);
|
|
309
|
+
this._setAuth(auth ?? '');
|
|
298
310
|
this._env = clientEnv;
|
|
299
311
|
// Set up callbacks if provided
|
|
300
312
|
if (onEvent)
|
|
@@ -305,9 +317,11 @@ export class RocketRideClient extends DAPClient {
|
|
|
305
317
|
this._callerOnDisconnected = onDisconnected;
|
|
306
318
|
if (onConnectError)
|
|
307
319
|
this._callerOnConnectError = onConnectError;
|
|
320
|
+
if (config.onTrace)
|
|
321
|
+
this._onTrace = config.onTrace;
|
|
308
322
|
// Set up persistence options
|
|
309
323
|
this._persist = persist ?? false;
|
|
310
|
-
|
|
324
|
+
// maxRetryTime accepted for backward compat but ignored (linear backoff never gives up)
|
|
311
325
|
}
|
|
312
326
|
/**
|
|
313
327
|
* Normalize a user-provided URI into a fully-formed HTTP/HTTPS URL.
|
|
@@ -325,7 +339,16 @@ export class RocketRideClient extends DAPClient {
|
|
|
325
339
|
}
|
|
326
340
|
try {
|
|
327
341
|
const url = new URL(normalized);
|
|
328
|
-
|
|
342
|
+
// The URL API silently strips ports that are default-for-scheme
|
|
343
|
+
// (e.g. :443 on https, :80 on http), so url.port alone cannot
|
|
344
|
+
// distinguish "no port given" from "scheme-default port given".
|
|
345
|
+
// Check the raw input for an explicit `:digits` after the scheme.
|
|
346
|
+
const withoutScheme = normalized.replace(/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//, '');
|
|
347
|
+
const authority = withoutScheme.split(/[/?#]/, 1)[0] ?? '';
|
|
348
|
+
const hasExplicitPort = authority.startsWith('[')
|
|
349
|
+
? /\]:\d+$/.test(authority) // IPv6 literal with explicit port
|
|
350
|
+
: /:\d+$/.test(authority); // hostname/IPv4 with explicit port
|
|
351
|
+
if (!url.port && !hasExplicitPort && !url.hostname.includes('rocketride.ai')) {
|
|
329
352
|
url.port = CONST_DEFAULT_WEB_PORT;
|
|
330
353
|
}
|
|
331
354
|
return `${url.protocol}//${url.host}`;
|
|
@@ -334,19 +357,57 @@ export class RocketRideClient extends DAPClient {
|
|
|
334
357
|
return normalized;
|
|
335
358
|
}
|
|
336
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Probe a server for its capabilities without authenticating.
|
|
362
|
+
*
|
|
363
|
+
* Creates a temporary public connection and sends an
|
|
364
|
+
* ``rrext_public_probe`` command. The server responds with version,
|
|
365
|
+
* capabilities, platform, and public apps without requiring credentials.
|
|
366
|
+
*
|
|
367
|
+
* @param uri - Server URI (e.g. ``"localhost:5565"``, ``"https://cloud.rocketride.ai"``)
|
|
368
|
+
* @param timeout - Optional timeout in ms for the entire operation
|
|
369
|
+
* @returns Server info including version and capability tags
|
|
370
|
+
* @throws Error if the server is unreachable or does not support probes
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* const info = await RocketRideClient.getServerInfo('localhost:5565');
|
|
375
|
+
* if (info.capabilities.includes('saas')) {
|
|
376
|
+
* // Show cloud sign-in options
|
|
377
|
+
* }
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
static async getServerInfo(uri, timeout) {
|
|
381
|
+
const client = new RocketRideClient({ uri, persist: false });
|
|
382
|
+
try {
|
|
383
|
+
// Open a public connection (no auth handshake)
|
|
384
|
+
await client.attach(uri, { timeout });
|
|
385
|
+
// Send rrext_public_probe — allowed on unauthenticated connections
|
|
386
|
+
const message = client.buildRequest('rrext_public_probe', {});
|
|
387
|
+
const response = await client.request(message, timeout);
|
|
388
|
+
if (response.success === false) {
|
|
389
|
+
throw new Error(response.message || 'Server info request failed');
|
|
390
|
+
}
|
|
391
|
+
return (response.body ?? {});
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
await client.disconnect();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
337
397
|
/**
|
|
338
398
|
* Normalize a user-provided URI into a fully-formed WebSocket address.
|
|
339
399
|
* Builds on normalizeUri, then converts to ws/wss and appends /task/service.
|
|
340
400
|
*/
|
|
341
401
|
_getWebsocketUri(uri) {
|
|
342
402
|
const httpUrl = RocketRideClient.normalizeUri(uri);
|
|
403
|
+
const path = this._wsPath ?? '/task/service';
|
|
343
404
|
try {
|
|
344
405
|
const url = new URL(httpUrl);
|
|
345
|
-
const wsScheme =
|
|
346
|
-
return `${wsScheme}//${url.host}
|
|
406
|
+
const wsScheme = url.protocol === 'https:' || url.protocol === 'wss:' ? 'wss:' : 'ws:';
|
|
407
|
+
return `${wsScheme}//${url.host}${path}`;
|
|
347
408
|
}
|
|
348
409
|
catch {
|
|
349
|
-
return `${httpUrl}
|
|
410
|
+
return `${httpUrl}${path}`;
|
|
350
411
|
}
|
|
351
412
|
}
|
|
352
413
|
/**
|
|
@@ -361,182 +422,318 @@ export class RocketRideClient extends DAPClient {
|
|
|
361
422
|
_setAuth(auth) {
|
|
362
423
|
this._apikey = auth;
|
|
363
424
|
}
|
|
364
|
-
/**
|
|
365
|
-
* Clear any pending reconnection timeout.
|
|
366
|
-
*/
|
|
367
|
-
_clearReconnectTimeout() {
|
|
368
|
-
if (this._reconnectTimeout) {
|
|
369
|
-
clearTimeout(this._reconnectTimeout);
|
|
370
|
-
this._reconnectTimeout = undefined;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
425
|
// ============================================================================
|
|
374
|
-
// CONNECTION
|
|
426
|
+
// INTERNAL CONNECTION HELPERS
|
|
375
427
|
// ============================================================================
|
|
376
428
|
/**
|
|
377
|
-
*
|
|
378
|
-
* calls DAPClient.connect (transport connect + auth handshake + onConnected).
|
|
429
|
+
* Create transport if needed and open the WebSocket. No auth.
|
|
379
430
|
*/
|
|
380
|
-
async
|
|
431
|
+
async _internalAttach(timeout) {
|
|
381
432
|
if (!this._transport) {
|
|
382
|
-
const transport = new TransportWebSocket(this._uri
|
|
433
|
+
const transport = new TransportWebSocket(this._uri);
|
|
383
434
|
this._bindTransport(transport);
|
|
384
435
|
}
|
|
385
|
-
await super.
|
|
436
|
+
await super._dapConnect(timeout);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Send the ``auth`` DAP command over the open transport.
|
|
440
|
+
* Sets ``_authenticated`` and ``_connectResult`` on success.
|
|
441
|
+
* Throws ``AuthenticationException`` on failure (transport stays open).
|
|
442
|
+
*/
|
|
443
|
+
async _internalLogin(timeout) {
|
|
444
|
+
// Build auth args with credential + client identification
|
|
445
|
+
const authArgs = { auth: this._apikey ?? '' };
|
|
446
|
+
if (this._clientDisplayName)
|
|
447
|
+
authArgs.clientName = this._clientDisplayName;
|
|
448
|
+
if (this._clientDisplayVersion)
|
|
449
|
+
authArgs.clientVersion = this._clientDisplayVersion;
|
|
450
|
+
const resp = await this.request({ type: 'request', command: 'auth', seq: 0, arguments: authArgs }, timeout);
|
|
451
|
+
const success = resp.success;
|
|
452
|
+
if (!success) {
|
|
453
|
+
throw new AuthenticationException(resp);
|
|
454
|
+
}
|
|
455
|
+
this._connectResult = resp.body;
|
|
456
|
+
this._authenticated = true;
|
|
457
|
+
// Store userToken for future reconnects
|
|
458
|
+
if (this._connectResult?.userToken) {
|
|
459
|
+
this._apikey = this._connectResult.userToken;
|
|
460
|
+
}
|
|
461
|
+
// Resubscribe monitors and notify
|
|
462
|
+
await this._resubscribeAllMonitors();
|
|
463
|
+
const connectionInfo = this._transport?.getConnectionInfo() ?? '';
|
|
464
|
+
if (this._callerOnConnected) {
|
|
465
|
+
try {
|
|
466
|
+
await this._callerOnConnected(connectionInfo);
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
this.debugMessage(`Error in user onConnected handler: ${e}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
await super.onConnected(connectionInfo);
|
|
473
|
+
return this._connectResult;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Send the ``deauth`` DAP command to revert to unauthenticated.
|
|
477
|
+
*/
|
|
478
|
+
async _internalLogout() {
|
|
479
|
+
if (!this._authenticated || !this._transport?.isConnected())
|
|
480
|
+
return;
|
|
481
|
+
try {
|
|
482
|
+
await this.request({ type: 'request', command: 'deauth', seq: 0, arguments: {} });
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
// Best-effort — server may have already disconnected
|
|
486
|
+
}
|
|
487
|
+
this._connectResult = undefined;
|
|
488
|
+
this._authenticated = false;
|
|
386
489
|
}
|
|
387
490
|
/**
|
|
388
|
-
*
|
|
389
|
-
* which triggers onDisconnected via the transport callback.
|
|
491
|
+
* Close the transport. Triggers onDisconnected via the transport callback.
|
|
390
492
|
*/
|
|
391
|
-
async _internalDisconnect(
|
|
493
|
+
async _internalDisconnect() {
|
|
392
494
|
if (!this._transport)
|
|
393
495
|
return;
|
|
394
|
-
await this._transport.disconnect(
|
|
496
|
+
await this._transport.disconnect();
|
|
395
497
|
}
|
|
396
498
|
/**
|
|
397
|
-
*
|
|
398
|
-
* reschedule with exponential backoff. Used by persist-mode connect() and
|
|
399
|
-
* by the reconnect timer.
|
|
499
|
+
* Clear the reconnect timer if active.
|
|
400
500
|
*/
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
this.
|
|
405
|
-
this.debugMessage('Connection successful');
|
|
501
|
+
_clearReconnectTimer() {
|
|
502
|
+
if (this._reconnectTimer) {
|
|
503
|
+
clearTimeout(this._reconnectTimer);
|
|
504
|
+
this._reconnectTimer = undefined;
|
|
406
505
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Reconnect engine driven by ``_desiredState``.
|
|
509
|
+
*
|
|
510
|
+
* Schedules a timer that re-attaches (and re-logins if the user had
|
|
511
|
+
* been authenticated). Checks ``_desiredState`` after every await so
|
|
512
|
+
* user actions mid-reconnect are respected immediately.
|
|
513
|
+
*
|
|
514
|
+
* Linear backoff: 250ms → 500ms → ... → 15 000ms cap.
|
|
515
|
+
*/
|
|
516
|
+
_scheduleReconnect() {
|
|
517
|
+
this.debugMessage(`Scheduling reconnect in ${this._currentReconnectDelay}ms`);
|
|
518
|
+
this._reconnectTimer = setTimeout(async () => {
|
|
519
|
+
try {
|
|
520
|
+
// Re-attach transport
|
|
521
|
+
await this._internalAttach();
|
|
522
|
+
if (this._desiredState === 'detached') {
|
|
523
|
+
this._reconnectTimer = undefined;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
// Re-login if the user was authenticated
|
|
527
|
+
if (this._desiredState === 'authenticated') {
|
|
528
|
+
await this._internalLogin();
|
|
529
|
+
if (this._desiredState === 'detached') {
|
|
530
|
+
this._reconnectTimer = undefined;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// Success — reset backoff
|
|
535
|
+
this._reconnectTimer = undefined;
|
|
536
|
+
this._currentReconnectDelay = 250;
|
|
537
|
+
this.debugMessage('Reconnect successful');
|
|
416
538
|
}
|
|
417
|
-
|
|
418
|
-
|
|
539
|
+
catch (err) {
|
|
540
|
+
// User changed intent — stop (desiredState may have been changed by detach() during await)
|
|
541
|
+
if (this._desiredState === 'detached') {
|
|
542
|
+
this._reconnectTimer = undefined;
|
|
419
543
|
return;
|
|
420
544
|
}
|
|
545
|
+
// Auth rejected — downgrade to attached, stop retrying auth
|
|
546
|
+
if (err instanceof AuthenticationException) {
|
|
547
|
+
this._desiredState = 'attached';
|
|
548
|
+
this._reconnectTimer = undefined;
|
|
549
|
+
await this.onConnectError(err);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
// Transient failure — linear backoff, cap at 15s
|
|
553
|
+
this._currentReconnectDelay = Math.min(this._currentReconnectDelay + 250, 15000);
|
|
554
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
555
|
+
await this.onConnectError(error);
|
|
556
|
+
this._scheduleReconnect(); // replaces timer with new delay
|
|
421
557
|
}
|
|
422
|
-
|
|
423
|
-
this._scheduleReconnect();
|
|
424
|
-
}
|
|
558
|
+
}, this._currentReconnectDelay);
|
|
425
559
|
}
|
|
560
|
+
// ============================================================================
|
|
561
|
+
// PUBLIC API — TRANSPORT
|
|
562
|
+
// ============================================================================
|
|
426
563
|
/**
|
|
427
|
-
*
|
|
564
|
+
* Attach to a RocketRide server (open WebSocket, no auth).
|
|
565
|
+
*
|
|
566
|
+
* If ``uri`` is provided and differs from the current URI, detaches
|
|
567
|
+
* first. If already attached to the same URI, this is a no-op.
|
|
568
|
+
*
|
|
569
|
+
* After attach, public APIs (``rrext_public_*``) are available.
|
|
570
|
+
*
|
|
571
|
+
* @param uri - Server URI override. Updates the stored URI if provided.
|
|
572
|
+
* @param options - Optional timeout for the WebSocket handshake.
|
|
428
573
|
*/
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
574
|
+
async attach(uri, options) {
|
|
575
|
+
// URI change → detach first, then update
|
|
576
|
+
if (uri) {
|
|
577
|
+
const normalised = this._getWebsocketUri(uri);
|
|
578
|
+
if (normalised !== this._uri) {
|
|
579
|
+
if (this.isAttached())
|
|
580
|
+
await this.detach();
|
|
581
|
+
this._setUri(uri);
|
|
435
582
|
}
|
|
436
583
|
}
|
|
437
|
-
|
|
438
|
-
this.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
584
|
+
// Already attached → no-op
|
|
585
|
+
if (this.isAttached()) {
|
|
586
|
+
this._desiredState = this._desiredState === 'detached' ? 'attached' : this._desiredState;
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this._desiredState = 'attached';
|
|
590
|
+
await this._internalAttach(options?.timeout);
|
|
444
591
|
}
|
|
445
592
|
/**
|
|
446
|
-
*
|
|
593
|
+
* Detach from the server (close WebSocket, cancel reconnection).
|
|
594
|
+
*
|
|
595
|
+
* Sets ``_desiredState`` to ``'detached'`` so the reconnect engine
|
|
596
|
+
* stops and ``onDisconnected`` does not restart it.
|
|
447
597
|
*/
|
|
448
|
-
|
|
598
|
+
async detach() {
|
|
599
|
+
this._desiredState = 'detached';
|
|
600
|
+
this._clearReconnectTimer();
|
|
601
|
+
this._authenticated = false;
|
|
602
|
+
this._connectResult = undefined;
|
|
603
|
+
if (this._transport?.isConnected()) {
|
|
604
|
+
await this._internalDisconnect();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* True when the WebSocket transport is connected (regardless of auth).
|
|
609
|
+
*/
|
|
610
|
+
isAttached() {
|
|
449
611
|
return this._transport?.isConnected() || false;
|
|
450
612
|
}
|
|
613
|
+
// ============================================================================
|
|
614
|
+
// PUBLIC API — AUTH
|
|
615
|
+
// ============================================================================
|
|
451
616
|
/**
|
|
452
|
-
*
|
|
617
|
+
* Authenticate over an attached transport.
|
|
453
618
|
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* (
|
|
457
|
-
*
|
|
619
|
+
* If ``uri`` is provided and differs, detaches and re-attaches first.
|
|
620
|
+
* If ``auth`` is provided and differs from the current credential,
|
|
621
|
+
* logs out (best-effort) before logging in with the new credential.
|
|
622
|
+
* If already authenticated with the same credential, this is a no-op.
|
|
623
|
+
*
|
|
624
|
+
* @param credential - API key, rr_ token, or PKCE code object.
|
|
625
|
+
* @param options - Optional URI override and/or timeout.
|
|
626
|
+
* @returns ConnectResult with user identity on success.
|
|
627
|
+
* @throws AuthenticationException on auth failure (transport stays attached).
|
|
458
628
|
*/
|
|
459
|
-
async
|
|
460
|
-
|
|
461
|
-
let
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
timeout = options;
|
|
465
|
-
}
|
|
466
|
-
else if (options) {
|
|
467
|
-
({ uri, auth, timeout } = options);
|
|
629
|
+
async login(credential, options) {
|
|
630
|
+
// Resolve credential
|
|
631
|
+
let resolvedCredential;
|
|
632
|
+
if (credential && typeof credential === 'object') {
|
|
633
|
+
resolvedCredential = 'cd_' + btoa(JSON.stringify(credential));
|
|
468
634
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
635
|
+
else {
|
|
636
|
+
const envKey = this._env['ROCKETRIDE_APIKEY'];
|
|
637
|
+
const envCredential = typeof envKey === 'string' && envKey.trim() !== '' ? envKey : undefined;
|
|
638
|
+
resolvedCredential = credential ?? envCredential ?? this._apikey ?? '';
|
|
639
|
+
}
|
|
640
|
+
// URI change → detach + re-attach
|
|
641
|
+
if (options?.uri) {
|
|
642
|
+
const normalised = this._getWebsocketUri(options.uri);
|
|
643
|
+
if (normalised !== this._uri) {
|
|
644
|
+
await this.detach();
|
|
645
|
+
this._setUri(options.uri);
|
|
646
|
+
await this._internalAttach(options.timeout);
|
|
647
|
+
}
|
|
472
648
|
}
|
|
473
|
-
|
|
474
|
-
|
|
649
|
+
// Ensure attached
|
|
650
|
+
if (!this.isAttached()) {
|
|
651
|
+
await this._internalAttach(options?.timeout);
|
|
475
652
|
}
|
|
476
|
-
|
|
477
|
-
this.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
if (this._persist) {
|
|
484
|
-
this._clearReconnectTimeout();
|
|
485
|
-
await this._attemptConnection(timeout);
|
|
653
|
+
// Auth change → logout first (best-effort)
|
|
654
|
+
if (resolvedCredential !== this._apikey && this._authenticated) {
|
|
655
|
+
try {
|
|
656
|
+
await this._internalLogout();
|
|
657
|
+
}
|
|
658
|
+
catch { }
|
|
486
659
|
}
|
|
487
|
-
|
|
488
|
-
|
|
660
|
+
this._setAuth(resolvedCredential);
|
|
661
|
+
// Already authenticated with same credential → no-op
|
|
662
|
+
if (this._authenticated) {
|
|
663
|
+
this._desiredState = 'authenticated';
|
|
664
|
+
return this._connectResult ?? {};
|
|
489
665
|
}
|
|
666
|
+
this._desiredState = 'authenticated';
|
|
667
|
+
return this._internalLogin(options?.timeout);
|
|
490
668
|
}
|
|
491
669
|
/**
|
|
492
|
-
*
|
|
670
|
+
* Deauthenticate: sends ``deauth`` to the server, clears client auth state.
|
|
671
|
+
* The transport stays attached — public APIs continue to work.
|
|
672
|
+
*/
|
|
673
|
+
async logout() {
|
|
674
|
+
await this._internalLogout();
|
|
675
|
+
this._desiredState = 'attached';
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* True when the auth handshake has succeeded on the current connection.
|
|
679
|
+
*/
|
|
680
|
+
isAuthenticated() {
|
|
681
|
+
return this._authenticated;
|
|
682
|
+
}
|
|
683
|
+
// ============================================================================
|
|
684
|
+
// COMPAT API — connect() / disconnect()
|
|
685
|
+
// ============================================================================
|
|
686
|
+
/**
|
|
687
|
+
* Check if the client is currently connected to the RocketRide server.
|
|
688
|
+
* Equivalent to ``isAttached()`` — kept for backward compatibility.
|
|
689
|
+
*/
|
|
690
|
+
isConnected() {
|
|
691
|
+
return this.isAttached();
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Connect to the RocketRide server and authenticate in a single call.
|
|
695
|
+
*
|
|
696
|
+
* Backward-compatible wrapper around ``attach()`` + ``login()``.
|
|
697
|
+
* Sends the credential as the first DAP message and returns the full
|
|
698
|
+
* ConnectResult (user identity + organizations + teams) on success.
|
|
493
699
|
*
|
|
494
|
-
*
|
|
700
|
+
* @param credential - API key / Zitadel access_token / rr_ user token / PKCE code object.
|
|
701
|
+
* @param options - Optional overrides: uri and/or timeout.
|
|
495
702
|
*/
|
|
496
|
-
async
|
|
497
|
-
this.
|
|
498
|
-
this.
|
|
499
|
-
|
|
500
|
-
await this._internalDisconnect();
|
|
501
|
-
}
|
|
703
|
+
async connect(credential, options) {
|
|
704
|
+
this._currentReconnectDelay = 250;
|
|
705
|
+
await this.attach(options?.uri, { timeout: options?.timeout });
|
|
706
|
+
return this.login(credential, options);
|
|
502
707
|
}
|
|
503
708
|
/**
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
* reconnection is scheduled only if we were connected.
|
|
709
|
+
* Get the ConnectResult from the last successful connect().
|
|
710
|
+
* Returns undefined if not connected or not yet authenticated.
|
|
507
711
|
*/
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
this._manualDisconnect = true;
|
|
517
|
-
this._clearReconnectTimeout();
|
|
518
|
-
if (wasAlreadyConnected) {
|
|
519
|
-
await this._internalDisconnect();
|
|
520
|
-
}
|
|
521
|
-
// Destroy transport so next connect() creates a new one with updated uri/auth (CONNECTION_LOGIC.md §2c)
|
|
522
|
-
if (options.uri !== undefined || options.auth !== undefined) {
|
|
523
|
-
this._transport = undefined;
|
|
524
|
-
}
|
|
525
|
-
if (this._persist && wasAlreadyConnected) {
|
|
526
|
-
this._manualDisconnect = false;
|
|
527
|
-
this._scheduleReconnect();
|
|
528
|
-
}
|
|
529
|
-
else if (wasAlreadyConnected) {
|
|
530
|
-
this._manualDisconnect = false;
|
|
531
|
-
await this._internalConnect();
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
this._manualDisconnect = false;
|
|
535
|
-
}
|
|
712
|
+
getAccountInfo() {
|
|
713
|
+
return this._connectResult;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Returns the ID of the user's primary organization.
|
|
717
|
+
*/
|
|
718
|
+
getOrgId() {
|
|
719
|
+
return this._connectResult?.organizations?.[0]?.id;
|
|
536
720
|
}
|
|
721
|
+
/**
|
|
722
|
+
* Disconnect from the RocketRide server and stop automatic reconnection.
|
|
723
|
+
* Backward-compatible wrapper around ``logout()`` + ``detach()``.
|
|
724
|
+
*/
|
|
725
|
+
async disconnect() {
|
|
726
|
+
await this.logout();
|
|
727
|
+
await this.detach();
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Update the environment variables used for pipeline substitution.
|
|
731
|
+
*
|
|
732
|
+
* The env dictionary is used by {@link use} and {@link validate} to replace
|
|
537
733
|
// ============================================================================
|
|
538
734
|
// PING METHODS
|
|
539
735
|
// ============================================================================
|
|
736
|
+
|
|
540
737
|
/**
|
|
541
738
|
* Test connectivity to the RocketRide server.
|
|
542
739
|
*
|
|
@@ -545,59 +742,16 @@ export class RocketRideClient extends DAPClient {
|
|
|
545
742
|
* and measuring response times.
|
|
546
743
|
*/
|
|
547
744
|
async ping(token) {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (this.didFail(response)) {
|
|
554
|
-
const errorMsg = response.message || 'Ping failed';
|
|
555
|
-
throw new Error(`Ping failed: ${errorMsg}`);
|
|
745
|
+
try {
|
|
746
|
+
await this.call('rrext_ping', undefined, { token });
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
throw new Error(`Ping failed: ${err instanceof Error ? err.message : err}`);
|
|
556
750
|
}
|
|
557
751
|
}
|
|
558
752
|
// ============================================================================
|
|
559
753
|
// EXECUTION METHODS
|
|
560
754
|
// ============================================================================
|
|
561
|
-
/**
|
|
562
|
-
* Substitute environment variables in a string.
|
|
563
|
-
* Replaces ${ROCKETRIDE_*} patterns with values from client's env dictionary.
|
|
564
|
-
* If variable is not found, leaves it unchanged.
|
|
565
|
-
*/
|
|
566
|
-
substituteEnvVars(value) {
|
|
567
|
-
// Match ${ROCKETRIDE_*} patterns
|
|
568
|
-
return value.replace(/\$\{(ROCKETRIDE_[^}]+)\}/g, (match, varName) => {
|
|
569
|
-
// Check if variable exists in client's env
|
|
570
|
-
if (varName in this._env) {
|
|
571
|
-
return String(this._env[varName]);
|
|
572
|
-
}
|
|
573
|
-
// If not found, leave as is
|
|
574
|
-
return match;
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Recursively process an object/array to substitute environment variables.
|
|
579
|
-
* Only processes string values, leaving other types unchanged.
|
|
580
|
-
*/
|
|
581
|
-
processEnvSubstitution(obj) {
|
|
582
|
-
if (typeof obj === 'string') {
|
|
583
|
-
// If it's a string, perform substitution
|
|
584
|
-
return this.substituteEnvVars(obj);
|
|
585
|
-
}
|
|
586
|
-
else if (Array.isArray(obj)) {
|
|
587
|
-
// If it's an array, process each element
|
|
588
|
-
return obj.map(item => this.processEnvSubstitution(item));
|
|
589
|
-
}
|
|
590
|
-
else if (obj !== null && typeof obj === 'object') {
|
|
591
|
-
// If it's an object, process each property
|
|
592
|
-
const result = {};
|
|
593
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
594
|
-
result[key] = this.processEnvSubstitution(value);
|
|
595
|
-
}
|
|
596
|
-
return result;
|
|
597
|
-
}
|
|
598
|
-
// For other types (number, boolean, null), return as is
|
|
599
|
-
return obj;
|
|
600
|
-
}
|
|
601
755
|
/**
|
|
602
756
|
* Load Node.js fs/promises at runtime without static imports.
|
|
603
757
|
* This keeps browser bundles free of Node built-ins while preserving Node features.
|
|
@@ -657,19 +811,16 @@ export class RocketRideClient extends DAPClient {
|
|
|
657
811
|
*/
|
|
658
812
|
async validate(options) {
|
|
659
813
|
const { pipeline, source } = options;
|
|
660
|
-
const
|
|
814
|
+
const args = { pipeline };
|
|
661
815
|
if (source !== undefined) {
|
|
662
|
-
|
|
816
|
+
args.source = source;
|
|
663
817
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const errorMsg = response.message || 'Validation failed';
|
|
670
|
-
throw new Error(`Pipeline validation failed: ${errorMsg}`);
|
|
818
|
+
try {
|
|
819
|
+
return await this.call('rrext_validate', args);
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
throw new Error(`Pipeline validation failed: ${err instanceof Error ? err.message : err}`);
|
|
671
823
|
}
|
|
672
|
-
return response.body || {};
|
|
673
824
|
}
|
|
674
825
|
// ============================================================================
|
|
675
826
|
// PIPELINE EXECUTION METHODS
|
|
@@ -718,7 +869,7 @@ export class RocketRideClient extends DAPClient {
|
|
|
718
869
|
* ```
|
|
719
870
|
*/
|
|
720
871
|
async use(options = {}) {
|
|
721
|
-
const { token, filepath, pipeline, source, threads, useExisting, args, ttl, pipelineTraceLevel } = options;
|
|
872
|
+
const { token, filepath, pipeline, source, threads, useExisting, args, ttl, pipelineTraceLevel, name, env, teamId } = options;
|
|
722
873
|
// Validate required parameters
|
|
723
874
|
if (!pipeline && !filepath) {
|
|
724
875
|
throw new Error('Pipeline configuration or file path is required and must be specified');
|
|
@@ -741,9 +892,7 @@ export class RocketRideClient extends DAPClient {
|
|
|
741
892
|
pipelineConfig = pipeline;
|
|
742
893
|
}
|
|
743
894
|
// Create a deep copy of the pipeline config to avoid modifying the original
|
|
744
|
-
|
|
745
|
-
// Perform environment variable substitution on the pipeline configuration
|
|
746
|
-
processedConfig = this.processEnvSubstitution(processedConfig);
|
|
895
|
+
const processedConfig = JSON.parse(JSON.stringify(pipelineConfig));
|
|
747
896
|
// Override source if specified (after substitution)
|
|
748
897
|
if (source !== undefined) {
|
|
749
898
|
processedConfig.source = source;
|
|
@@ -770,54 +919,135 @@ export class RocketRideClient extends DAPClient {
|
|
|
770
919
|
if (pipelineTraceLevel !== undefined) {
|
|
771
920
|
arguments_.pipelineTraceLevel = pipelineTraceLevel;
|
|
772
921
|
}
|
|
922
|
+
// Build ROCKETRIDE_* env from client's .env + caller overrides
|
|
923
|
+
const rocketEnv = {};
|
|
924
|
+
for (const [k, v] of Object.entries(this._env)) {
|
|
925
|
+
if (k.startsWith('ROCKETRIDE_'))
|
|
926
|
+
rocketEnv[k] = v;
|
|
927
|
+
}
|
|
928
|
+
if (env)
|
|
929
|
+
Object.assign(rocketEnv, env);
|
|
930
|
+
if (Object.keys(rocketEnv).length > 0) {
|
|
931
|
+
arguments_.env = rocketEnv;
|
|
932
|
+
}
|
|
933
|
+
// Derive display name from filepath if not explicitly provided
|
|
934
|
+
const effectiveName = name ?? (filepath ? filepath.replace(/^.*[\\/]/, '').replace(/\.pipe(?:\.json)?$/, '') : undefined);
|
|
935
|
+
if (effectiveName !== undefined) {
|
|
936
|
+
arguments_.name = effectiveName;
|
|
937
|
+
}
|
|
938
|
+
if (teamId !== undefined) {
|
|
939
|
+
arguments_.teamId = teamId;
|
|
940
|
+
}
|
|
773
941
|
// Send execution request to server
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
942
|
+
try {
|
|
943
|
+
const body = await this.call('execute', arguments_);
|
|
944
|
+
// Extract and validate response
|
|
945
|
+
const responseBody = body || {};
|
|
946
|
+
const taskToken = responseBody.token;
|
|
947
|
+
if (!taskToken) {
|
|
948
|
+
throw new Error('Server did not return a task token in successful response');
|
|
949
|
+
}
|
|
950
|
+
this.debugMessage(`Pipeline execution started successfully, task token: ${taskToken}`);
|
|
951
|
+
// Type assertion to ensure token is present
|
|
952
|
+
return responseBody;
|
|
781
953
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
throw new Error('Server did not return a task token in successful response');
|
|
954
|
+
catch (err) {
|
|
955
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
956
|
+
this.debugMessage(`Pipeline execution failed: ${errorMsg}`);
|
|
957
|
+
throw err;
|
|
787
958
|
}
|
|
788
|
-
this.debugMessage(`Pipeline execution started successfully, task token: ${taskToken}`);
|
|
789
|
-
// Type assertion to ensure token is present
|
|
790
|
-
return responseBody;
|
|
791
959
|
}
|
|
792
960
|
/**
|
|
793
961
|
* Terminate a running pipeline.
|
|
794
962
|
*/
|
|
795
963
|
async terminate(token) {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const errorMsg = response.message || 'Unknown termination error';
|
|
964
|
+
try {
|
|
965
|
+
await this.call('terminate', undefined, { token });
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
802
969
|
this.debugMessage(`Pipeline termination failed: ${errorMsg}`);
|
|
803
970
|
throw new Error(errorMsg);
|
|
804
971
|
}
|
|
805
972
|
}
|
|
973
|
+
/**
|
|
974
|
+
* Restart a running pipeline with a new configuration.
|
|
975
|
+
*
|
|
976
|
+
* Looks up the existing task by project/source, terminates it, and
|
|
977
|
+
* starts a new execution in one server round-trip.
|
|
978
|
+
*
|
|
979
|
+
* @param options.token - Existing task token (optional, resolved server-side if omitted).
|
|
980
|
+
* @param options.projectId - The project identifier.
|
|
981
|
+
* @param options.source - The source component identifier.
|
|
982
|
+
* @param options.pipeline - The pipeline configuration to restart with.
|
|
983
|
+
*/
|
|
984
|
+
async restart(options) {
|
|
985
|
+
try {
|
|
986
|
+
await this.call('restart', {
|
|
987
|
+
token: options.token,
|
|
988
|
+
projectId: options.projectId,
|
|
989
|
+
source: options.source,
|
|
990
|
+
pipeline: options.pipeline,
|
|
991
|
+
}, { token: '*' });
|
|
992
|
+
}
|
|
993
|
+
catch (err) {
|
|
994
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
995
|
+
this.debugMessage(`Pipeline restart failed: ${errorMsg}`);
|
|
996
|
+
throw new Error(errorMsg);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
806
999
|
/**
|
|
807
1000
|
* Get the current status of a running pipeline.
|
|
1001
|
+
*
|
|
1002
|
+
* By default this call is bounded to 15s so callers/tests don't hang forever if the engine
|
|
1003
|
+
* stops responding mid-request (especially important in CI). Pass `{ timeout: false }` to
|
|
1004
|
+
* restore the previous behavior of using only the client-level request timeout (if any).
|
|
808
1005
|
*/
|
|
809
|
-
async getTaskStatus(token) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1006
|
+
async getTaskStatus(token, options) {
|
|
1007
|
+
try {
|
|
1008
|
+
const callOptions = { token };
|
|
1009
|
+
if (options?.timeout === false) {
|
|
1010
|
+
// Intentionally omit per-call timeout override.
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
callOptions.timeout = options?.timeout ?? 15000;
|
|
1014
|
+
}
|
|
1015
|
+
return await this.call('rrext_get_task_status', undefined, callOptions);
|
|
1016
|
+
}
|
|
1017
|
+
catch (err) {
|
|
1018
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
816
1019
|
this.debugMessage(`Pipeline status retrieval failed: ${errorMsg}`);
|
|
817
1020
|
throw new Error(errorMsg);
|
|
818
1021
|
}
|
|
819
|
-
|
|
820
|
-
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Resolve a running task's token from its project ID and source component.
|
|
1025
|
+
*
|
|
1026
|
+
* The token is required for operations like terminate and restart.
|
|
1027
|
+
* Returns undefined if no task is currently running for the given project/source.
|
|
1028
|
+
*
|
|
1029
|
+
* @param options.projectId - The project identifier.
|
|
1030
|
+
* @param options.source - The source component identifier.
|
|
1031
|
+
*/
|
|
1032
|
+
async getTaskToken(options) {
|
|
1033
|
+
const body = await this.call('rrext_get_token', {
|
|
1034
|
+
projectId: options.projectId,
|
|
1035
|
+
source: options.source,
|
|
1036
|
+
});
|
|
1037
|
+
return body?.token;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Returns the unresolved pipeline for a running task.
|
|
1041
|
+
*
|
|
1042
|
+
* The pipeline is returned exactly as stored — ${ROCKETRIDE_*} placeholders are
|
|
1043
|
+
* NOT substituted, so no secrets are included in the response.
|
|
1044
|
+
*
|
|
1045
|
+
* @param token - Task token returned by {@link getTaskToken}.
|
|
1046
|
+
* @returns The unresolved pipeline dict, or undefined if the task is not found.
|
|
1047
|
+
*/
|
|
1048
|
+
async getTaskPipeline(token) {
|
|
1049
|
+
const body = await this.call('rrext_get_pipeline', undefined, { token });
|
|
1050
|
+
return body?.pipeline;
|
|
821
1051
|
}
|
|
822
1052
|
// ============================================================================
|
|
823
1053
|
// DATA METHODS
|
|
@@ -912,7 +1142,7 @@ export class RocketRideClient extends DAPClient {
|
|
|
912
1142
|
event: 'apaevt_status_upload',
|
|
913
1143
|
body: body,
|
|
914
1144
|
seq: 0,
|
|
915
|
-
type: 'event'
|
|
1145
|
+
type: 'event',
|
|
916
1146
|
};
|
|
917
1147
|
this.onEvent(eventMessage);
|
|
918
1148
|
};
|
|
@@ -1004,7 +1234,7 @@ export class RocketRideClient extends DAPClient {
|
|
|
1004
1234
|
results[index] = finalResult;
|
|
1005
1235
|
};
|
|
1006
1236
|
// Create a promise for every file - let server handle queuing
|
|
1007
|
-
const uploadPromises = files.map((fileData, index) => uploadFile(fileData, index).catch(err => {
|
|
1237
|
+
const uploadPromises = files.map((fileData, index) => uploadFile(fileData, index).catch((err) => {
|
|
1008
1238
|
// Ensure errors don't kill the whole batch
|
|
1009
1239
|
console.error(`Upload failed for ${fileData.file.name}:`, err);
|
|
1010
1240
|
}));
|
|
@@ -1106,6 +1336,13 @@ export class RocketRideClient extends DAPClient {
|
|
|
1106
1336
|
const seqNum = message.seq || 0;
|
|
1107
1337
|
// Forward to debugging interface if available
|
|
1108
1338
|
this._sendVSCodeEvent(eventType, eventBody);
|
|
1339
|
+
// Update cached ConnectResult when the server pushes a full account refresh
|
|
1340
|
+
if (eventType === 'apaext_account') {
|
|
1341
|
+
this._connectResult = eventBody;
|
|
1342
|
+
if (this._connectResult?.userToken) {
|
|
1343
|
+
this._apikey = this._connectResult.userToken;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1109
1346
|
// Dispatch pipe-scoped SSE events to the registered DataPipe callback
|
|
1110
1347
|
if (eventType === 'apaevt_sse') {
|
|
1111
1348
|
const pipeId = eventBody?.pipe_id;
|
|
@@ -1142,7 +1379,8 @@ export class RocketRideClient extends DAPClient {
|
|
|
1142
1379
|
async onConnectError(error) {
|
|
1143
1380
|
if (this._callerOnConnectError) {
|
|
1144
1381
|
try {
|
|
1145
|
-
|
|
1382
|
+
const connectionError = error instanceof ConnectionException ? error : new ConnectionException({ message: String(error) });
|
|
1383
|
+
await this._callerOnConnectError(connectionError);
|
|
1146
1384
|
}
|
|
1147
1385
|
catch (e) {
|
|
1148
1386
|
this.debugMessage(`Error in user onConnectError handler: ${e}`);
|
|
@@ -1151,564 +1389,697 @@ export class RocketRideClient extends DAPClient {
|
|
|
1151
1389
|
await super.onConnectError(error);
|
|
1152
1390
|
}
|
|
1153
1391
|
/**
|
|
1154
|
-
* Handle connected
|
|
1392
|
+
* Handle transport-level connected event.
|
|
1393
|
+
*
|
|
1394
|
+
* With the attach/login split, this fires when the WebSocket opens
|
|
1395
|
+
* (before auth). The ``_internalLogin`` method handles the auth
|
|
1396
|
+
* notification separately, so this is intentionally minimal.
|
|
1155
1397
|
*/
|
|
1156
1398
|
async onConnected(connectionInfo) {
|
|
1157
|
-
this._manualDisconnect = false;
|
|
1158
|
-
this._didNotifyConnected = true;
|
|
1159
|
-
this._clearReconnectTimeout();
|
|
1160
|
-
this._currentReconnectDelay = 250;
|
|
1161
|
-
this._retryStartTime = undefined;
|
|
1162
|
-
// Call user-provided event handler if available
|
|
1163
|
-
if (this._callerOnConnected) {
|
|
1164
|
-
try {
|
|
1165
|
-
await this._callerOnConnected(connectionInfo);
|
|
1166
|
-
}
|
|
1167
|
-
catch (error) {
|
|
1168
|
-
// Log errors but don't let user code break the connection
|
|
1169
|
-
this.debugMessage(`Error in user onConnected handler for ${connectionInfo}: ${error}`);
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
1399
|
await super.onConnected(connectionInfo);
|
|
1173
1400
|
}
|
|
1174
1401
|
/**
|
|
1175
|
-
* Handle
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1402
|
+
* Handle transport disconnection.
|
|
1403
|
+
*
|
|
1404
|
+
* Clears transport and auth state, notifies the user callback,
|
|
1405
|
+
* then consults ``_desiredState`` to decide whether to reconnect.
|
|
1178
1406
|
*/
|
|
1179
1407
|
async onDisconnected(reason, hasError) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1408
|
+
// Transport is gone — clear so next attach creates a fresh one
|
|
1409
|
+
this._transport = undefined;
|
|
1410
|
+
this._connectResult = undefined;
|
|
1411
|
+
this._authenticated = false;
|
|
1412
|
+
// Notify user callback
|
|
1413
|
+
if (this._callerOnDisconnected) {
|
|
1414
|
+
try {
|
|
1415
|
+
await this._callerOnDisconnected(reason, hasError);
|
|
1416
|
+
}
|
|
1417
|
+
catch (error) {
|
|
1418
|
+
this.debugMessage(`Error in user onDisconnected handler for ${reason}: ${error}`);
|
|
1190
1419
|
}
|
|
1191
|
-
// Chain to parent to clear pending requests
|
|
1192
|
-
await super.onDisconnected(reason, hasError);
|
|
1193
1420
|
}
|
|
1194
|
-
//
|
|
1195
|
-
|
|
1196
|
-
|
|
1421
|
+
// Chain to parent to clear pending requests
|
|
1422
|
+
await super.onDisconnected(reason, hasError);
|
|
1423
|
+
// Reconnect engine: honour _desiredState
|
|
1424
|
+
if (this._desiredState === 'detached')
|
|
1425
|
+
return;
|
|
1426
|
+
if (!this._persist) {
|
|
1427
|
+
this._desiredState = 'detached';
|
|
1428
|
+
return;
|
|
1197
1429
|
}
|
|
1430
|
+
if (this._reconnectTimer)
|
|
1431
|
+
return; // engine already active
|
|
1432
|
+
this._currentReconnectDelay = 250;
|
|
1433
|
+
this._scheduleReconnect();
|
|
1198
1434
|
}
|
|
1199
1435
|
/**
|
|
1200
1436
|
* Subscribe to specific types of events from the server.
|
|
1437
|
+
* @deprecated Use {@link addMonitor} / {@link removeMonitor} instead.
|
|
1201
1438
|
*/
|
|
1202
1439
|
async setEvents(token, eventTypes, pipeId) {
|
|
1203
|
-
// Build event subscription
|
|
1440
|
+
// Build event subscription args
|
|
1204
1441
|
const args = { types: eventTypes };
|
|
1205
1442
|
if (pipeId !== undefined)
|
|
1206
1443
|
args.pipeId = pipeId;
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
const response = await this.request(request);
|
|
1213
|
-
// Check for errors
|
|
1214
|
-
if (this.didFail(response)) {
|
|
1215
|
-
const errorMsg = response.message || 'Event subscription failed';
|
|
1216
|
-
throw new Error(errorMsg);
|
|
1444
|
+
try {
|
|
1445
|
+
await this.call('rrext_monitor', args, { token });
|
|
1446
|
+
}
|
|
1447
|
+
catch (err) {
|
|
1448
|
+
throw new Error(`Event subscription failed: ${err instanceof Error ? err.message : err}`);
|
|
1217
1449
|
}
|
|
1218
1450
|
}
|
|
1219
1451
|
// ============================================================================
|
|
1220
|
-
//
|
|
1452
|
+
// MONITOR SUBSCRIPTION MANAGEMENT
|
|
1221
1453
|
// ============================================================================
|
|
1222
1454
|
/**
|
|
1223
|
-
*
|
|
1455
|
+
* Add a monitor subscription. If the key already exists, the new types are
|
|
1456
|
+
* merged via reference counting and the merged set is sent to the server.
|
|
1224
1457
|
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
* you're updating the version you expect (prevents conflicts).
|
|
1228
|
-
*
|
|
1229
|
-
* @param options - Save project options
|
|
1230
|
-
* @param options.projectId - Unique identifier for the project
|
|
1231
|
-
* @param options.pipeline - Pipeline configuration object
|
|
1232
|
-
* @param options.expectedVersion - Expected current version for atomic updates (optional)
|
|
1233
|
-
* @returns Promise resolving to save result with success status, projectId, and new version
|
|
1234
|
-
* @throws Error if save fails due to version mismatch, storage error, or invalid input
|
|
1235
|
-
*
|
|
1236
|
-
* @example
|
|
1237
|
-
* ```typescript
|
|
1238
|
-
* // Save a new project
|
|
1239
|
-
* const result = await client.saveProject({
|
|
1240
|
-
* projectId: 'proj-123',
|
|
1241
|
-
* pipeline: {
|
|
1242
|
-
* name: 'Data Processor',
|
|
1243
|
-
* source: 'source_1',
|
|
1244
|
-
* components: [...]
|
|
1245
|
-
* }
|
|
1246
|
-
* });
|
|
1247
|
-
* console.log(`Saved version: ${result.version}`);
|
|
1248
|
-
*
|
|
1249
|
-
* // Update existing project with version check
|
|
1250
|
-
* const existing = await client.getProject({ projectId: 'proj-123' });
|
|
1251
|
-
* existing.name = 'Updated Name';
|
|
1252
|
-
* const updated = await client.saveProject({
|
|
1253
|
-
* projectId: 'proj-123',
|
|
1254
|
-
* pipeline: existing,
|
|
1255
|
-
* expectedVersion: existing.version
|
|
1256
|
-
* });
|
|
1257
|
-
* ```
|
|
1458
|
+
* @param key - Monitor key: `{ token }` for a running task, or `{ projectId, source }` for a project.
|
|
1459
|
+
* @param types - Event types to subscribe to (e.g. `['summary', 'flow']`).
|
|
1258
1460
|
*/
|
|
1259
|
-
async
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
if (!
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1461
|
+
async addMonitor(key, types) {
|
|
1462
|
+
const keyStr = this._monitorKeyToString(key);
|
|
1463
|
+
let refCounts = this._monitorKeys.get(keyStr);
|
|
1464
|
+
if (!refCounts) {
|
|
1465
|
+
refCounts = new Map();
|
|
1466
|
+
this._monitorKeys.set(keyStr, refCounts);
|
|
1467
|
+
}
|
|
1468
|
+
// Increment reference counts
|
|
1469
|
+
for (const t of types) {
|
|
1470
|
+
refCounts.set(t, (refCounts.get(t) ?? 0) + 1);
|
|
1471
|
+
}
|
|
1472
|
+
// Send merged types to server — rollback on failure
|
|
1473
|
+
try {
|
|
1474
|
+
await this._syncMonitor(key, refCounts);
|
|
1267
1475
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
if (this.didFail(response)) {
|
|
1283
|
-
const errorMsg = response.message || 'Unknown error saving project';
|
|
1284
|
-
this.debugMessage(`Project save failed: ${errorMsg}`);
|
|
1285
|
-
throw new Error(errorMsg);
|
|
1476
|
+
catch (error) {
|
|
1477
|
+
for (const t of types) {
|
|
1478
|
+
const current = refCounts.get(t) ?? 0;
|
|
1479
|
+
if (current <= 1) {
|
|
1480
|
+
refCounts.delete(t);
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
refCounts.set(t, current - 1);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (refCounts.size === 0) {
|
|
1487
|
+
this._monitorKeys.delete(keyStr);
|
|
1488
|
+
}
|
|
1489
|
+
throw error;
|
|
1286
1490
|
}
|
|
1287
|
-
// Extract and return response
|
|
1288
|
-
this.debugMessage(`Project saved successfully: ${projectId}, version: ${response.body?.version}`);
|
|
1289
|
-
return response.body;
|
|
1290
1491
|
}
|
|
1291
1492
|
/**
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
* Fetches the complete pipeline configuration and current version for
|
|
1295
|
-
* the specified project. Use this before updating to get the current
|
|
1296
|
-
* version for atomic updates.
|
|
1297
|
-
*
|
|
1298
|
-
* @param options - Get project options
|
|
1299
|
-
* @param options.projectId - Unique identifier of the project to retrieve
|
|
1300
|
-
* @returns Promise resolving to project data with success status, pipeline, and version
|
|
1301
|
-
* @throws Error if project doesn't exist or retrieval fails
|
|
1302
|
-
*
|
|
1303
|
-
* @example
|
|
1304
|
-
* ```typescript
|
|
1305
|
-
* // Get a project
|
|
1306
|
-
* try {
|
|
1307
|
-
* const project = await client.getProject({ projectId: 'proj-123' });
|
|
1308
|
-
* console.log(`Project: ${project.name}`);
|
|
1309
|
-
* console.log(`Version: ${project.version}`);
|
|
1310
|
-
* } catch (error) {
|
|
1311
|
-
* if (error.message.includes('NOT_FOUND')) {
|
|
1312
|
-
* console.log("Project doesn't exist");
|
|
1313
|
-
* }
|
|
1314
|
-
* }
|
|
1493
|
+
* Remove a monitor subscription. Decrements reference counts for the given
|
|
1494
|
+
* types. Only unsubscribes a type from the server when its count reaches 0.
|
|
1315
1495
|
*
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
* project.name = 'Updated';
|
|
1319
|
-
* await client.saveProject({
|
|
1320
|
-
* projectId: 'proj-123',
|
|
1321
|
-
* pipeline: project,
|
|
1322
|
-
* expectedVersion: project.version
|
|
1323
|
-
* });
|
|
1324
|
-
* ```
|
|
1496
|
+
* @param key - Monitor key (must match the key used in addMonitor).
|
|
1497
|
+
* @param types - Event types to unsubscribe from.
|
|
1325
1498
|
*/
|
|
1326
|
-
async
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
if (!
|
|
1330
|
-
|
|
1499
|
+
async removeMonitor(key, types) {
|
|
1500
|
+
const keyStr = this._monitorKeyToString(key);
|
|
1501
|
+
const refCounts = this._monitorKeys.get(keyStr);
|
|
1502
|
+
if (!refCounts)
|
|
1503
|
+
return;
|
|
1504
|
+
// Decrement reference counts
|
|
1505
|
+
for (const t of types) {
|
|
1506
|
+
const current = refCounts.get(t) ?? 0;
|
|
1507
|
+
if (current <= 1) {
|
|
1508
|
+
refCounts.delete(t);
|
|
1509
|
+
}
|
|
1510
|
+
else {
|
|
1511
|
+
refCounts.set(t, current - 1);
|
|
1512
|
+
}
|
|
1331
1513
|
}
|
|
1332
|
-
//
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
// Send request to server
|
|
1338
|
-
const request = this.buildRequest('rrext_store', { arguments: args });
|
|
1339
|
-
const response = await this.request(request);
|
|
1340
|
-
// Check for errors
|
|
1341
|
-
if (this.didFail(response)) {
|
|
1342
|
-
const errorMsg = response.message || 'Unknown error retrieving project';
|
|
1343
|
-
this.debugMessage(`Project retrieval failed: ${errorMsg}`);
|
|
1344
|
-
throw new Error(errorMsg);
|
|
1514
|
+
// Send merged types (or unsubscribe if empty)
|
|
1515
|
+
await this._syncMonitor(key, refCounts);
|
|
1516
|
+
// Clean up empty keys
|
|
1517
|
+
if (refCounts.size === 0) {
|
|
1518
|
+
this._monitorKeys.delete(keyStr);
|
|
1345
1519
|
}
|
|
1346
|
-
// Extract and return response
|
|
1347
|
-
this.debugMessage(`Project retrieved successfully: ${projectId}`);
|
|
1348
|
-
return response.body;
|
|
1349
1520
|
}
|
|
1350
1521
|
/**
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1353
|
-
* Permanently removes a project from storage. Optionally verify the
|
|
1354
|
-
* version before deletion to ensure you're deleting the version you
|
|
1355
|
-
* expect (prevents accidental deletion of modified projects).
|
|
1356
|
-
*
|
|
1357
|
-
* @param options - Delete project options
|
|
1358
|
-
* @param options.projectId - Unique identifier of the project to delete
|
|
1359
|
-
* @param options.expectedVersion - Expected current version for atomic deletion (required)
|
|
1360
|
-
* @returns Promise resolving to deletion result with success status and message
|
|
1361
|
-
* @throws Error if project doesn't exist, version mismatch, or deletion fails
|
|
1362
|
-
*
|
|
1363
|
-
* @example
|
|
1364
|
-
* ```typescript
|
|
1365
|
-
* // Safe deletion with version check
|
|
1366
|
-
* const project = await client.getProject({ projectId: 'proj-123' });
|
|
1367
|
-
* try {
|
|
1368
|
-
* const result = await client.deleteProject({
|
|
1369
|
-
* projectId: 'proj-123',
|
|
1370
|
-
* expectedVersion: project.version
|
|
1371
|
-
* });
|
|
1372
|
-
* console.log('Project deleted successfully');
|
|
1373
|
-
* } catch (error) {
|
|
1374
|
-
* if (error.message.includes('CONFLICT')) {
|
|
1375
|
-
* console.log('Project was modified, deletion cancelled');
|
|
1376
|
-
* }
|
|
1377
|
-
* }
|
|
1378
|
-
* ```
|
|
1522
|
+
* Send the merged type list for a monitor key to the server.
|
|
1379
1523
|
*/
|
|
1380
|
-
async
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1524
|
+
async _syncMonitor(key, refCounts) {
|
|
1525
|
+
if (!this.isConnected())
|
|
1526
|
+
return;
|
|
1527
|
+
const mergedTypes = Array.from(refCounts.keys());
|
|
1528
|
+
if ('token' in key) {
|
|
1529
|
+
await this.call('rrext_monitor', { types: mergedTypes }, { token: key.token });
|
|
1385
1530
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
const request = this.buildRequest('rrext_store', { arguments: args });
|
|
1397
|
-
const response = await this.request(request);
|
|
1398
|
-
// Check for errors
|
|
1399
|
-
if (this.didFail(response)) {
|
|
1400
|
-
const errorMsg = response.message || 'Unknown error deleting project';
|
|
1401
|
-
this.debugMessage(`Project deletion failed: ${errorMsg}`);
|
|
1402
|
-
throw new Error(errorMsg);
|
|
1531
|
+
else {
|
|
1532
|
+
const args = {
|
|
1533
|
+
projectId: key.projectId,
|
|
1534
|
+
source: key.source,
|
|
1535
|
+
types: mergedTypes,
|
|
1536
|
+
};
|
|
1537
|
+
if (key.pipeId !== undefined) {
|
|
1538
|
+
args.pipeId = key.pipeId;
|
|
1539
|
+
}
|
|
1540
|
+
await this.call('rrext_monitor', args);
|
|
1403
1541
|
}
|
|
1404
|
-
// Extract and return response
|
|
1405
|
-
this.debugMessage(`Project deleted successfully: ${projectId}`);
|
|
1406
|
-
return response.body;
|
|
1407
1542
|
}
|
|
1408
1543
|
/**
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
* Retrieves a summary of all projects stored for the authenticated user.
|
|
1412
|
-
* Each project summary includes the ID, name, list of data sources, and total component count.
|
|
1413
|
-
*
|
|
1414
|
-
* @returns Promise resolving to list result with success status, projects array, and count
|
|
1415
|
-
* @throws Error if retrieval fails
|
|
1416
|
-
*
|
|
1417
|
-
* @example
|
|
1418
|
-
* ```typescript
|
|
1419
|
-
* // List all projects
|
|
1420
|
-
* const result = await client.getAllProjects();
|
|
1421
|
-
* console.log(`Found ${result.count} projects:`);
|
|
1422
|
-
* for (const project of result.projects) {
|
|
1423
|
-
* console.log(`- ${project.id}: ${project.name} (${project.totalComponents} components)`);
|
|
1424
|
-
* for (const source of project.sources) {
|
|
1425
|
-
* console.log(` * ${source.name} (${source.provider})`);
|
|
1426
|
-
* }
|
|
1427
|
-
* }
|
|
1428
|
-
*
|
|
1429
|
-
* // Find specific project
|
|
1430
|
-
* const result = await client.getAllProjects();
|
|
1431
|
-
* const myProject = result.projects.find(p => p.id === 'proj-123');
|
|
1432
|
-
* ```
|
|
1544
|
+
* Replay all active monitor subscriptions to the server.
|
|
1545
|
+
* Called automatically after reconnection.
|
|
1433
1546
|
*/
|
|
1434
|
-
async
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1547
|
+
async _resubscribeAllMonitors() {
|
|
1548
|
+
for (const [keyStr, refCounts] of this._monitorKeys) {
|
|
1549
|
+
if (refCounts.size === 0)
|
|
1550
|
+
continue;
|
|
1551
|
+
const key = this._monitorStringToKey(keyStr);
|
|
1552
|
+
if (key) {
|
|
1553
|
+
try {
|
|
1554
|
+
await this._syncMonitor(key, refCounts);
|
|
1555
|
+
}
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
this.debugMessage(`Failed to resubscribe monitor ${keyStr}: ${error}`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Convert a MonitorKey to a stable string for map lookup.
|
|
1564
|
+
*/
|
|
1565
|
+
_monitorKeyToString(key) {
|
|
1566
|
+
if ('token' in key) {
|
|
1567
|
+
return `t:${key.token}`;
|
|
1568
|
+
}
|
|
1569
|
+
let s = `p:${key.projectId}.${key.source}`;
|
|
1570
|
+
if (key.pipeId !== undefined) {
|
|
1571
|
+
s += `.${key.pipeId}`;
|
|
1447
1572
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1573
|
+
return s;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Reverse a key-string back to a MonitorKey (for resubscribeAll).
|
|
1577
|
+
*/
|
|
1578
|
+
_monitorStringToKey(keyStr) {
|
|
1579
|
+
if (keyStr.startsWith('t:')) {
|
|
1580
|
+
return { token: keyStr.slice(2) };
|
|
1581
|
+
}
|
|
1582
|
+
if (keyStr.startsWith('p:')) {
|
|
1583
|
+
const rest = keyStr.slice(2);
|
|
1584
|
+
const dotIdx = rest.indexOf('.');
|
|
1585
|
+
if (dotIdx === -1)
|
|
1586
|
+
return null;
|
|
1587
|
+
const projectId = rest.slice(0, dotIdx);
|
|
1588
|
+
const remaining = rest.slice(dotIdx + 1);
|
|
1589
|
+
const parts = remaining.split('.');
|
|
1590
|
+
if (parts.length === 2 && !isNaN(Number(parts[1]))) {
|
|
1591
|
+
return { projectId, source: parts[0], pipeId: Number(parts[1]) };
|
|
1592
|
+
}
|
|
1593
|
+
return { projectId, source: remaining };
|
|
1594
|
+
}
|
|
1595
|
+
return null;
|
|
1452
1596
|
}
|
|
1453
1597
|
// ============================================================================
|
|
1454
|
-
// TEMPLATE STORAGE MANAGEMENT (
|
|
1598
|
+
// TEMPLATE STORAGE MANAGEMENT (convenience wrappers using fsReadJson/fsWriteJson)
|
|
1455
1599
|
// ============================================================================
|
|
1456
1600
|
/**
|
|
1457
|
-
*
|
|
1601
|
+
* Persist a pipeline configuration as a named template in the account store.
|
|
1458
1602
|
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
* Use expectedVersion to ensure you're updating the version you expect.
|
|
1603
|
+
* Templates are stored as JSON files under `.templates/<templateId>.json`.
|
|
1604
|
+
* Saving a template with an existing ID overwrites the previous version.
|
|
1462
1605
|
*
|
|
1463
|
-
* @param options -
|
|
1464
|
-
* @param options.
|
|
1465
|
-
* @
|
|
1466
|
-
* @param options.expectedVersion - Expected current version for atomic updates (optional)
|
|
1467
|
-
* @returns Promise resolving to save result with success status, templateId, and new version
|
|
1468
|
-
* @throws Error if save fails due to version mismatch, storage error, or invalid input
|
|
1606
|
+
* @param options.templateId - Unique identifier for the template (no path separators)
|
|
1607
|
+
* @param options.pipeline - Pipeline configuration object to save
|
|
1608
|
+
* @throws Error if templateId is invalid or pipeline is not a non-empty object
|
|
1469
1609
|
*/
|
|
1470
1610
|
async saveTemplate(options) {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
}
|
|
1476
|
-
if (!pipeline || typeof pipeline !== 'object') {
|
|
1611
|
+
// Validate the template ID to prevent path traversal or invalid filenames
|
|
1612
|
+
this.validateId(options.templateId, 'templateId');
|
|
1613
|
+
// Ensure the pipeline payload is a non-null object before writing
|
|
1614
|
+
if (!options.pipeline || typeof options.pipeline !== 'object')
|
|
1477
1615
|
throw new Error('pipeline must be a non-empty object');
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
const args = {
|
|
1481
|
-
subcommand: 'save_template',
|
|
1482
|
-
templateId,
|
|
1483
|
-
pipeline,
|
|
1484
|
-
};
|
|
1485
|
-
// Add optional version for atomic updates
|
|
1486
|
-
if (expectedVersion !== undefined) {
|
|
1487
|
-
args.expectedVersion = expectedVersion;
|
|
1488
|
-
}
|
|
1489
|
-
// Send request to server
|
|
1490
|
-
const request = this.buildRequest('rrext_store', { arguments: args });
|
|
1491
|
-
const response = await this.request(request);
|
|
1492
|
-
// Check for errors
|
|
1493
|
-
if (this.didFail(response)) {
|
|
1494
|
-
const errorMsg = response.message || 'Unknown error saving template';
|
|
1495
|
-
this.debugMessage(`Template save failed: ${errorMsg}`);
|
|
1496
|
-
throw new Error(errorMsg);
|
|
1497
|
-
}
|
|
1498
|
-
// Extract and return response
|
|
1499
|
-
this.debugMessage(`Template saved successfully: ${templateId}, version: ${response.body?.version}`);
|
|
1500
|
-
return response.body;
|
|
1616
|
+
// Serialise and write the pipeline under the .templates virtual directory
|
|
1617
|
+
await this.fsWriteJson(`.templates/${options.templateId}.json`, options.pipeline);
|
|
1501
1618
|
}
|
|
1502
1619
|
/**
|
|
1503
|
-
* Retrieve a template
|
|
1620
|
+
* Retrieve a previously saved pipeline template from the account store.
|
|
1621
|
+
*
|
|
1622
|
+
* @param options.templateId - Unique identifier of the template to retrieve
|
|
1623
|
+
* @returns The pipeline configuration object that was saved
|
|
1624
|
+
* @throws Error if the template does not exist or templateId is invalid
|
|
1504
1625
|
*/
|
|
1505
1626
|
async getTemplate(options) {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
}
|
|
1511
|
-
// Build request
|
|
1512
|
-
const args = {
|
|
1513
|
-
subcommand: 'get_template',
|
|
1514
|
-
templateId,
|
|
1515
|
-
};
|
|
1516
|
-
// Send request to server
|
|
1517
|
-
const request = this.buildRequest('rrext_store', { arguments: args });
|
|
1518
|
-
const response = await this.request(request);
|
|
1519
|
-
// Check for errors
|
|
1520
|
-
if (this.didFail(response)) {
|
|
1521
|
-
const errorMsg = response.message || 'Unknown error retrieving template';
|
|
1522
|
-
this.debugMessage(`Template retrieval failed: ${errorMsg}`);
|
|
1523
|
-
throw new Error(errorMsg);
|
|
1524
|
-
}
|
|
1525
|
-
// Extract and return response
|
|
1526
|
-
this.debugMessage(`Template retrieved successfully: ${templateId}`);
|
|
1527
|
-
return response.body;
|
|
1627
|
+
// Validate the ID before constructing the storage path
|
|
1628
|
+
this.validateId(options.templateId, 'templateId');
|
|
1629
|
+
// Read and parse the JSON file from the .templates virtual directory
|
|
1630
|
+
return this.fsReadJson(`.templates/${options.templateId}.json`);
|
|
1528
1631
|
}
|
|
1529
1632
|
/**
|
|
1530
|
-
* Delete a template
|
|
1633
|
+
* Delete a pipeline template from the account store.
|
|
1634
|
+
*
|
|
1635
|
+
* @param options.templateId - Unique identifier of the template to delete
|
|
1636
|
+
* @throws Error if the template does not exist or templateId is invalid
|
|
1531
1637
|
*/
|
|
1532
1638
|
async deleteTemplate(options) {
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
}
|
|
1538
|
-
// Build request
|
|
1539
|
-
const args = {
|
|
1540
|
-
subcommand: 'delete_template',
|
|
1541
|
-
templateId,
|
|
1542
|
-
};
|
|
1543
|
-
// Add optional version for atomic deletion
|
|
1544
|
-
if (expectedVersion !== undefined) {
|
|
1545
|
-
args.expectedVersion = expectedVersion;
|
|
1546
|
-
}
|
|
1547
|
-
// Send request to server
|
|
1548
|
-
const request = this.buildRequest('rrext_store', { arguments: args });
|
|
1549
|
-
const response = await this.request(request);
|
|
1550
|
-
// Check for errors
|
|
1551
|
-
if (this.didFail(response)) {
|
|
1552
|
-
const errorMsg = response.message || 'Unknown error deleting template';
|
|
1553
|
-
this.debugMessage(`Template deletion failed: ${errorMsg}`);
|
|
1554
|
-
throw new Error(errorMsg);
|
|
1555
|
-
}
|
|
1556
|
-
// Extract and return response
|
|
1557
|
-
this.debugMessage(`Template deleted successfully: ${templateId}`);
|
|
1558
|
-
return response.body;
|
|
1639
|
+
// Validate the ID before constructing the storage path
|
|
1640
|
+
this.validateId(options.templateId, 'templateId');
|
|
1641
|
+
// Delete the JSON file from the .templates virtual directory
|
|
1642
|
+
await this.fsDelete(`.templates/${options.templateId}.json`);
|
|
1559
1643
|
}
|
|
1560
1644
|
/**
|
|
1561
|
-
* List all templates.
|
|
1645
|
+
* List all pipeline templates stored in the account store.
|
|
1646
|
+
*
|
|
1647
|
+
* Reads the `.templates` directory, parses each `.json` file, and extracts
|
|
1648
|
+
* a summary for each template. Files that cannot be parsed are silently
|
|
1649
|
+
* skipped so a single corrupt template does not break the entire listing.
|
|
1650
|
+
*
|
|
1651
|
+
* @returns Array of template summaries sorted in directory-listing order.
|
|
1652
|
+
* Each entry contains the template ID, display name, source components,
|
|
1653
|
+
* and total component count.
|
|
1562
1654
|
*/
|
|
1563
1655
|
async getAllTemplates() {
|
|
1564
|
-
//
|
|
1565
|
-
const
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1656
|
+
// Fetch the list of entries under the .templates virtual directory
|
|
1657
|
+
const dir = await this.fsListDir('.templates');
|
|
1658
|
+
const templates = [];
|
|
1659
|
+
for (const entry of dir.entries) {
|
|
1660
|
+
// Skip directories and any non-JSON files (e.g. temp files)
|
|
1661
|
+
if (entry.type !== 'file' || !entry.name.endsWith('.json'))
|
|
1662
|
+
continue;
|
|
1663
|
+
try {
|
|
1664
|
+
// Derive the template ID by stripping the .json extension
|
|
1665
|
+
const id = entry.name.slice(0, -5);
|
|
1666
|
+
// Load and parse the template JSON
|
|
1667
|
+
const pipeline = await this.fsReadJson(`.templates/${entry.name}`);
|
|
1668
|
+
// Extract Source-mode components to populate the sources summary list
|
|
1669
|
+
const sources = (pipeline.components || []).filter((c) => c.config?.mode === 'Source').map((c) => ({ id: c.id, provider: c.provider, name: c.config?.name || c.id }));
|
|
1670
|
+
// Push the summary (use template ID as display name)
|
|
1671
|
+
templates.push({ id, name: id, sources, totalComponents: (pipeline.components || []).length });
|
|
1672
|
+
}
|
|
1673
|
+
catch (err) {
|
|
1674
|
+
// Log the failure but continue so one bad file doesn't block others
|
|
1675
|
+
console.debug(`[RocketRideClient] Failed to read .templates/${entry.name}:`, err);
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1576
1678
|
}
|
|
1577
|
-
|
|
1578
|
-
const templateCount = response.body?.count || 0;
|
|
1579
|
-
this.debugMessage(`Templates retrieved successfully: ${templateCount} templates`);
|
|
1580
|
-
return response.body;
|
|
1679
|
+
return templates;
|
|
1581
1680
|
}
|
|
1582
1681
|
// ============================================================================
|
|
1583
|
-
// LOG STORAGE MANAGEMENT (
|
|
1682
|
+
// LOG STORAGE MANAGEMENT (convenience wrappers using fsReadJson/fsWriteJson)
|
|
1584
1683
|
// ============================================================================
|
|
1585
1684
|
/**
|
|
1586
|
-
*
|
|
1685
|
+
* Persist a pipeline execution log to the account store.
|
|
1686
|
+
*
|
|
1687
|
+
* Logs are stored under `.logs/<projectId>/<source>-<startTime>.log`.
|
|
1688
|
+
* The filename is derived from `contents.body.startTime` so logs are
|
|
1689
|
+
* naturally sortable by execution start time.
|
|
1690
|
+
*
|
|
1691
|
+
* @param options.projectId - Project identifier that owns this log
|
|
1692
|
+
* @param options.source - Source component identifier the log is associated with
|
|
1693
|
+
* @param options.contents - Log payload; must contain `body.startTime`
|
|
1694
|
+
* @returns The generated filename (e.g. `"ingest-1714000000000.log"`)
|
|
1695
|
+
* @throws Error if any ID is invalid, contents is not an object, or startTime is missing
|
|
1587
1696
|
*/
|
|
1588
1697
|
async saveLog(options) {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
if (!source) {
|
|
1595
|
-
throw new Error('source is required');
|
|
1596
|
-
}
|
|
1597
|
-
if (!contents || typeof contents !== 'object') {
|
|
1698
|
+
// Validate identifiers to prevent path traversal
|
|
1699
|
+
this.validateId(options.projectId, 'projectId');
|
|
1700
|
+
this.validateId(options.source, 'source');
|
|
1701
|
+
// Ensure the contents payload is a non-null object
|
|
1702
|
+
if (!options.contents || typeof options.contents !== 'object')
|
|
1598
1703
|
throw new Error('contents must be a non-empty object');
|
|
1599
|
-
|
|
1600
|
-
//
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
this.debugMessage(`Log saved successfully: ${response.body?.filename}`);
|
|
1618
|
-
return response.body;
|
|
1704
|
+
// startTime is required; it forms part of the filename for chronological ordering.
|
|
1705
|
+
// Reject anything other than a non-empty number or numeric-looking string to
|
|
1706
|
+
// prevent path-separator chars from slipping into the generated filename.
|
|
1707
|
+
const startTime = options.contents?.body?.startTime;
|
|
1708
|
+
if (startTime === undefined || startTime === null)
|
|
1709
|
+
throw new Error('contents must contain body.startTime');
|
|
1710
|
+
if (typeof startTime !== 'number' && typeof startTime !== 'string') {
|
|
1711
|
+
throw new Error('contents.body.startTime must be a number or string');
|
|
1712
|
+
}
|
|
1713
|
+
const startTimeStr = String(startTime);
|
|
1714
|
+
if (!startTimeStr || /[\\/]/.test(startTimeStr)) {
|
|
1715
|
+
throw new Error('contents.body.startTime must not be empty or contain path separators');
|
|
1716
|
+
}
|
|
1717
|
+
// Construct a deterministic filename from source and start time
|
|
1718
|
+
const filename = `${options.source}-${startTimeStr}.log`;
|
|
1719
|
+
// Write the log JSON to the per-project logs directory
|
|
1720
|
+
await this.fsWriteJson(`.logs/${options.projectId}/${filename}`, options.contents);
|
|
1721
|
+
return filename;
|
|
1619
1722
|
}
|
|
1620
1723
|
/**
|
|
1621
|
-
*
|
|
1724
|
+
* Retrieve a previously saved pipeline execution log from the account store.
|
|
1725
|
+
*
|
|
1726
|
+
* @param options.projectId - Project identifier that owns the log
|
|
1727
|
+
* @param options.name - Filename of the log (as returned by saveLog)
|
|
1728
|
+
* @returns The log payload that was saved
|
|
1729
|
+
* @throws Error if the log does not exist or projectId is invalid
|
|
1622
1730
|
*/
|
|
1623
1731
|
async getLog(options) {
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
if (!
|
|
1627
|
-
throw new Error('
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
// Check for errors
|
|
1646
|
-
if (this.didFail(response)) {
|
|
1647
|
-
const errorMsg = response.message || 'Unknown error retrieving log';
|
|
1648
|
-
this.debugMessage(`Log retrieval failed: ${errorMsg}`);
|
|
1649
|
-
throw new Error(errorMsg);
|
|
1650
|
-
}
|
|
1651
|
-
// Extract and return response
|
|
1652
|
-
this.debugMessage(`Log retrieved successfully: ${projectId}/${source}`);
|
|
1653
|
-
return response.body;
|
|
1732
|
+
// Validate the project ID before constructing the storage path
|
|
1733
|
+
this.validateId(options.projectId, 'projectId');
|
|
1734
|
+
if (!options.name)
|
|
1735
|
+
throw new Error('name is required');
|
|
1736
|
+
// Read and parse the log JSON from the per-project logs directory
|
|
1737
|
+
return this.fsReadJson(`.logs/${options.projectId}/${options.name}`);
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Delete a pipeline execution log from the account store.
|
|
1741
|
+
*
|
|
1742
|
+
* @param options.projectId - Project identifier that owns the log
|
|
1743
|
+
* @param options.name - Filename of the log to delete
|
|
1744
|
+
* @throws Error if the log does not exist or projectId is invalid
|
|
1745
|
+
*/
|
|
1746
|
+
async deleteLog(options) {
|
|
1747
|
+
// Validate the project ID before constructing the storage path
|
|
1748
|
+
this.validateId(options.projectId, 'projectId');
|
|
1749
|
+
if (!options.name)
|
|
1750
|
+
throw new Error('name is required');
|
|
1751
|
+
// Delete the log file from the per-project logs directory
|
|
1752
|
+
await this.fsDelete(`.logs/${options.projectId}/${options.name}`);
|
|
1654
1753
|
}
|
|
1655
1754
|
/**
|
|
1656
|
-
* List
|
|
1755
|
+
* List pipeline execution logs stored for a project, optionally filtered by source.
|
|
1756
|
+
*
|
|
1757
|
+
* Results are sorted ascending by `modified` timestamp so the oldest log
|
|
1758
|
+
* appears first. The caller can page through or slice the array as needed.
|
|
1759
|
+
*
|
|
1760
|
+
* @param options.projectId - Project identifier whose logs to list
|
|
1761
|
+
* @param options.source - Optional source component filter; when set, only logs
|
|
1762
|
+
* whose filename starts with `<source>-` are returned
|
|
1763
|
+
* @returns Array of log name and optional modified timestamp, sorted oldest-first
|
|
1764
|
+
* @throws Error if projectId (or source when provided) is invalid
|
|
1657
1765
|
*/
|
|
1658
1766
|
async listLogs(options) {
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
if (
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1767
|
+
// Validate identifiers before constructing the storage path
|
|
1768
|
+
this.validateId(options.projectId, 'projectId');
|
|
1769
|
+
if (options.source)
|
|
1770
|
+
this.validateId(options.source, 'source');
|
|
1771
|
+
// List all entries in the per-project logs directory
|
|
1772
|
+
const dir = await this.fsListDir(`.logs/${options.projectId}`);
|
|
1773
|
+
// Keep only .log files and map to the public shape (name + modified)
|
|
1774
|
+
let logs = dir.entries.filter((e) => e.type === 'file' && e.name.endsWith('.log')).map((e) => ({ name: e.name, modified: e.modified }));
|
|
1775
|
+
// Apply optional source prefix filter when a source was specified
|
|
1776
|
+
if (options.source) {
|
|
1777
|
+
logs = logs.filter((l) => l.name.startsWith(`${options.source}-`));
|
|
1778
|
+
}
|
|
1779
|
+
// Sort ascending by modified timestamp; treat missing timestamps as epoch 0
|
|
1780
|
+
logs.sort((a, b) => (a.modified || 0) - (b.modified || 0));
|
|
1781
|
+
return logs;
|
|
1782
|
+
}
|
|
1783
|
+
// ============================================================================
|
|
1784
|
+
// HANDLE-BASED FILE STORE OPERATIONS
|
|
1785
|
+
// ============================================================================
|
|
1786
|
+
/**
|
|
1787
|
+
* Open a file handle for reading or writing.
|
|
1788
|
+
*
|
|
1789
|
+
* @param path - Relative path within the account store
|
|
1790
|
+
* @param mode - 'r' for read, 'w' for write (default: 'r')
|
|
1791
|
+
* @param offset - Initial byte offset (read mode only)
|
|
1792
|
+
* @returns Object with 'handle' (string). Read mode also includes 'size' (number).
|
|
1793
|
+
*/
|
|
1794
|
+
async fsOpen(path, mode = 'r') {
|
|
1795
|
+
this.validateStorePath(path);
|
|
1796
|
+
return this.call('rrext_store', { subcommand: 'fs_open', path, mode });
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Read data from an open read handle.
|
|
1800
|
+
*
|
|
1801
|
+
* @param handle - Handle ID returned by fsOpen
|
|
1802
|
+
* @param offset - Byte offset to read from
|
|
1803
|
+
* @param length - Max bytes to read (default 4 MB). Empty Uint8Array indicates EOF.
|
|
1804
|
+
* @returns The bytes read
|
|
1805
|
+
*/
|
|
1806
|
+
async fsRead(handle, offset = 0, length = 4194304) {
|
|
1807
|
+
// Bypass call() which unwraps response.body, losing response.arguments
|
|
1808
|
+
// where the server places the binary data payload.
|
|
1809
|
+
const message = this.buildRequest('rrext_store', {
|
|
1810
|
+
arguments: { subcommand: 'fs_read', handle, offset, length },
|
|
1811
|
+
});
|
|
1812
|
+
this._onTrace?.(TraceType.Request, message);
|
|
1813
|
+
const response = await this.request(message);
|
|
1814
|
+
if (response.success === false) {
|
|
1815
|
+
this._onTrace?.(TraceType.Error, response);
|
|
1816
|
+
throw new Error(response.message ?? 'fs_read failed');
|
|
1817
|
+
}
|
|
1818
|
+
this._onTrace?.(TraceType.Success, response);
|
|
1819
|
+
return response.arguments?.data || new Uint8Array(0);
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Write data to an open write handle.
|
|
1823
|
+
*
|
|
1824
|
+
* @param handle - Handle ID returned by fsOpen
|
|
1825
|
+
* @param data - Raw bytes to write
|
|
1826
|
+
* @returns Number of bytes written
|
|
1827
|
+
*/
|
|
1828
|
+
async fsWrite(handle, data) {
|
|
1829
|
+
const body = await this.call('rrext_store', { subcommand: 'fs_write', handle, data });
|
|
1830
|
+
return body?.bytesWritten ?? 0;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Close a file handle.
|
|
1834
|
+
*
|
|
1835
|
+
* @param handle - Handle ID returned by fsOpen
|
|
1836
|
+
* @param mode - 'r' or 'w' (must match the mode used in fsOpen)
|
|
1837
|
+
*/
|
|
1838
|
+
async fsClose(handle, mode) {
|
|
1839
|
+
await this.call('rrext_store', { subcommand: 'fs_close', handle, mode });
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Delete a file.
|
|
1843
|
+
*
|
|
1844
|
+
* @param path - Relative path within the account store
|
|
1845
|
+
* @throws Error if file does not exist or delete fails
|
|
1846
|
+
*/
|
|
1847
|
+
async fsDelete(path) {
|
|
1848
|
+
this.validateStorePath(path);
|
|
1849
|
+
await this.call('rrext_store', { subcommand: 'fs_delete', path });
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* List immediate children of a directory.
|
|
1853
|
+
*
|
|
1854
|
+
* @param path - Relative directory path (default: account root)
|
|
1855
|
+
* @returns Directory entries with name and type (file or dir)
|
|
1856
|
+
*/
|
|
1857
|
+
async fsListDir(path = '') {
|
|
1858
|
+
if (path)
|
|
1859
|
+
this.validateStorePath(path);
|
|
1860
|
+
return this.call('rrext_store', { subcommand: 'fs_list_dir', path });
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Create a directory.
|
|
1864
|
+
*
|
|
1865
|
+
* @param path - Relative directory path
|
|
1866
|
+
*/
|
|
1867
|
+
async fsMkdir(path) {
|
|
1868
|
+
this.validateStorePath(path);
|
|
1869
|
+
await this.call('rrext_store', { subcommand: 'fs_mkdir', path });
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Remove a directory.
|
|
1873
|
+
*
|
|
1874
|
+
* @param path - Relative directory path
|
|
1875
|
+
* @param recursive - If true, delete contents recursively (default: false)
|
|
1876
|
+
* @throws Error if directory is not empty (when recursive is false) or delete fails
|
|
1877
|
+
*/
|
|
1878
|
+
async fsRmdir(path, recursive = false) {
|
|
1879
|
+
this.validateStorePath(path);
|
|
1880
|
+
await this.call('rrext_store', { subcommand: 'fs_rmdir', path, recursive });
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Get file or directory metadata.
|
|
1884
|
+
*
|
|
1885
|
+
* @param path - Relative path within the account store
|
|
1886
|
+
* @returns Metadata including existence, type, size (bytes), and modified epoch timestamp (for files)
|
|
1887
|
+
*/
|
|
1888
|
+
async fsStat(path) {
|
|
1889
|
+
this.validateStorePath(path);
|
|
1890
|
+
return this.call('rrext_store', { subcommand: 'fs_stat', path });
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Rename a file or directory.
|
|
1894
|
+
*
|
|
1895
|
+
* On object stores this is implemented as copy + delete. For directories,
|
|
1896
|
+
* all contents are moved recursively.
|
|
1897
|
+
*
|
|
1898
|
+
* @param oldPath - Current relative path within the account store
|
|
1899
|
+
* @param newPath - New relative path within the account store
|
|
1900
|
+
* @throws Error if oldPath does not exist or rename fails
|
|
1901
|
+
*/
|
|
1902
|
+
async fsRename(oldPath, newPath) {
|
|
1903
|
+
this.validateStorePath(oldPath);
|
|
1904
|
+
this.validateStorePath(newPath);
|
|
1905
|
+
await this.call('rrext_store', { subcommand: 'fs_rename', old_path: oldPath, new_path: newPath });
|
|
1906
|
+
}
|
|
1907
|
+
// ============================================================================
|
|
1908
|
+
// CONVENIENCE WRAPPERS (text/JSON over binary, handle open/close internally)
|
|
1909
|
+
// ============================================================================
|
|
1910
|
+
/** Read a file as a UTF-8 string. */
|
|
1911
|
+
async fsReadString(path) {
|
|
1912
|
+
const { handle } = await this.fsOpen(path, 'r');
|
|
1913
|
+
try {
|
|
1914
|
+
const chunks = [];
|
|
1915
|
+
let offset = 0;
|
|
1916
|
+
while (true) {
|
|
1917
|
+
const chunk = await this.fsRead(handle, offset);
|
|
1918
|
+
if (chunk.length === 0)
|
|
1919
|
+
break;
|
|
1920
|
+
chunks.push(chunk);
|
|
1921
|
+
offset += chunk.length;
|
|
1922
|
+
}
|
|
1923
|
+
const total = new Uint8Array(offset);
|
|
1924
|
+
let pos = 0;
|
|
1925
|
+
for (const chunk of chunks) {
|
|
1926
|
+
total.set(chunk, pos);
|
|
1927
|
+
pos += chunk.length;
|
|
1928
|
+
}
|
|
1929
|
+
return new TextDecoder().decode(total);
|
|
1672
1930
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1931
|
+
finally {
|
|
1932
|
+
await this.fsClose(handle, 'r');
|
|
1675
1933
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
this.
|
|
1683
|
-
|
|
1934
|
+
}
|
|
1935
|
+
/** Write a UTF-8 string to a file. */
|
|
1936
|
+
async fsWriteString(path, text) {
|
|
1937
|
+
const { handle } = await this.fsOpen(path, 'w');
|
|
1938
|
+
try {
|
|
1939
|
+
await this.fsWrite(handle, new TextEncoder().encode(text));
|
|
1940
|
+
await this.fsClose(handle, 'w');
|
|
1941
|
+
}
|
|
1942
|
+
catch (err) {
|
|
1943
|
+
try {
|
|
1944
|
+
await this.fsClose(handle, 'w');
|
|
1945
|
+
}
|
|
1946
|
+
catch {
|
|
1947
|
+
/* best-effort */
|
|
1948
|
+
}
|
|
1949
|
+
throw err;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
/** Read a JSON file. */
|
|
1953
|
+
async fsReadJson(path) {
|
|
1954
|
+
const text = await this.fsReadString(path);
|
|
1955
|
+
return JSON.parse(text);
|
|
1956
|
+
}
|
|
1957
|
+
/** Write an object as JSON. */
|
|
1958
|
+
async fsWriteJson(path, obj) {
|
|
1959
|
+
await this.fsWriteString(path, JSON.stringify(obj, null, 2));
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Validate a relative path intended for the account file store.
|
|
1963
|
+
*
|
|
1964
|
+
* Splits the path on `/` (after normalising backslashes) and checks every
|
|
1965
|
+
* segment for path-traversal attempts (`..`) and forbidden characters.
|
|
1966
|
+
* Empty segments (from leading/trailing/double slashes) are skipped because
|
|
1967
|
+
* they carry no security risk on the server side.
|
|
1968
|
+
*
|
|
1969
|
+
* @param path - Relative path to validate (e.g. `.templates/my-pipe.json`)
|
|
1970
|
+
* @throws Error if any segment is `..` or contains illegal characters
|
|
1971
|
+
*/
|
|
1972
|
+
validateStorePath(path) {
|
|
1973
|
+
// Normalise Windows-style backslashes to forward slashes before splitting
|
|
1974
|
+
for (const segment of path.replace(/\\/g, '/').split('/')) {
|
|
1975
|
+
// Reject parent-directory traversal attempts in any position of the path
|
|
1976
|
+
if (segment === '..')
|
|
1977
|
+
throw new Error(`Path traversal not allowed: ${path}`);
|
|
1978
|
+
// Only validate non-empty segments (empty ones arise from leading/trailing slashes)
|
|
1979
|
+
if (segment) {
|
|
1980
|
+
for (const ch of segment) {
|
|
1981
|
+
// Reject forbidden metacharacters and ASCII control characters (< 0x20)
|
|
1982
|
+
if (RocketRideClient.INVALID_PATH_CHARS.has(ch) || ch.charCodeAt(0) < 0x20) {
|
|
1983
|
+
throw new Error(`Path contains invalid characters: ${path}`);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Validate a single identifier (projectId, source, templateId, etc.) used
|
|
1991
|
+
* to construct store paths.
|
|
1992
|
+
*
|
|
1993
|
+
* IDs must be non-empty strings that contain no path separators and no
|
|
1994
|
+
* characters from the forbidden set. This prevents an ID from escaping its
|
|
1995
|
+
* intended directory when interpolated into a path.
|
|
1996
|
+
*
|
|
1997
|
+
* @param value - The identifier string to validate
|
|
1998
|
+
* @param name - Human-readable field name used in error messages (e.g. `"projectId"`)
|
|
1999
|
+
* @throws Error if value is empty, contains path separators, or contains illegal characters
|
|
2000
|
+
*/
|
|
2001
|
+
validateId(value, name) {
|
|
2002
|
+
// Require a non-empty value
|
|
2003
|
+
if (!value)
|
|
2004
|
+
throw new Error(`${name} is required`);
|
|
2005
|
+
// Reject forward and backward slashes to prevent path injection
|
|
2006
|
+
if (value.includes('/') || value.includes('\\'))
|
|
2007
|
+
throw new Error(`${name} must not contain path separators`);
|
|
2008
|
+
// Reject any forbidden metacharacter or ASCII control character
|
|
2009
|
+
for (const ch of value) {
|
|
2010
|
+
if (RocketRideClient.INVALID_PATH_CHARS.has(ch) || ch.charCodeAt(0) < 0x20) {
|
|
2011
|
+
throw new Error(`${name} contains invalid characters: ${value}`);
|
|
2012
|
+
}
|
|
1684
2013
|
}
|
|
1685
|
-
// Extract and return response
|
|
1686
|
-
const logCount = response.body?.total_count || 0;
|
|
1687
|
-
this.debugMessage(`Logs retrieved successfully: ${logCount} logs`);
|
|
1688
|
-
return response.body;
|
|
1689
2014
|
}
|
|
1690
2015
|
// ============================================================================
|
|
1691
|
-
//
|
|
2016
|
+
// DASHBOARD METHODS
|
|
1692
2017
|
// ============================================================================
|
|
1693
2018
|
/**
|
|
1694
|
-
*
|
|
2019
|
+
* Retrieve a server dashboard snapshot.
|
|
1695
2020
|
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
* to the underlying request() method.
|
|
2021
|
+
* Returns the current state of all connections, tasks, and aggregate
|
|
2022
|
+
* metrics from the server. Requires 'task.monitor' permission.
|
|
1699
2023
|
*
|
|
1700
|
-
* @
|
|
1701
|
-
* @param args - Optional arguments for the command
|
|
1702
|
-
* @param token - Optional task/session token
|
|
1703
|
-
* @param timeout - Optional per-request timeout in ms
|
|
1704
|
-
* @returns The response DAPMessage from the server
|
|
2024
|
+
* @returns DashboardResponse containing overview, connections, and tasks
|
|
1705
2025
|
*/
|
|
1706
|
-
async
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2026
|
+
async getDashboard() {
|
|
2027
|
+
return this.call('rrext_dashboard', {});
|
|
2028
|
+
}
|
|
2029
|
+
// ============================================================================
|
|
2030
|
+
// CPROFILE PROFILING
|
|
2031
|
+
// ============================================================================
|
|
2032
|
+
/**
|
|
2033
|
+
* Start a cProfile profiling session on the server process or a pipeline.
|
|
2034
|
+
*
|
|
2035
|
+
* @param target - Task token to profile a pipeline subprocess, or
|
|
2036
|
+
* undefined/null to profile the server process itself.
|
|
2037
|
+
* @param session - Optional human-readable session name.
|
|
2038
|
+
* @returns Status object with session info and start time.
|
|
2039
|
+
*/
|
|
2040
|
+
async cprofileStart(target, session) {
|
|
2041
|
+
const args = {};
|
|
2042
|
+
if (target)
|
|
2043
|
+
args.target = target;
|
|
2044
|
+
if (session)
|
|
2045
|
+
args.session = session;
|
|
2046
|
+
return this.call('rrext_cprofile_start', args);
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Stop the active cProfile profiling session.
|
|
2050
|
+
*
|
|
2051
|
+
* @param target - Task token if profiling a pipeline, or undefined for server.
|
|
2052
|
+
* @returns Result with session name and runtime.
|
|
2053
|
+
*/
|
|
2054
|
+
async cprofileStop(target) {
|
|
2055
|
+
const args = {};
|
|
2056
|
+
if (target)
|
|
2057
|
+
args.target = target;
|
|
2058
|
+
return this.call('rrext_cprofile_stop', args);
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Get the current cProfile profiling status.
|
|
2062
|
+
*
|
|
2063
|
+
* @param target - Task token if querying a pipeline, or undefined for server.
|
|
2064
|
+
* @returns Status indicating active/inactive, owner, runtime.
|
|
2065
|
+
*/
|
|
2066
|
+
async cprofileStatus(target) {
|
|
2067
|
+
const args = {};
|
|
2068
|
+
if (target)
|
|
2069
|
+
args.target = target;
|
|
2070
|
+
return this.call('rrext_cprofile_status', args);
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Get the full cProfile report from the last completed session.
|
|
2074
|
+
*
|
|
2075
|
+
* @param target - Task token if querying a pipeline, or undefined for server.
|
|
2076
|
+
* @returns Object containing the full pstats text report.
|
|
2077
|
+
*/
|
|
2078
|
+
async cprofileReport(target) {
|
|
2079
|
+
const args = {};
|
|
2080
|
+
if (target)
|
|
2081
|
+
args.target = target;
|
|
2082
|
+
return this.call('rrext_cprofile_report', args);
|
|
1712
2083
|
}
|
|
1713
2084
|
// ============================================================================
|
|
1714
2085
|
// CONTEXT MANAGER SUPPORT - Python-style async context manager
|
|
@@ -1727,7 +2098,7 @@ export class RocketRideClient extends DAPClient {
|
|
|
1727
2098
|
static async withConnection(config, callback) {
|
|
1728
2099
|
const client = new RocketRideClient(config);
|
|
1729
2100
|
try {
|
|
1730
|
-
await client.connect();
|
|
2101
|
+
await client.connect(config.auth);
|
|
1731
2102
|
return await callback(client);
|
|
1732
2103
|
}
|
|
1733
2104
|
finally {
|
|
@@ -1764,17 +2135,7 @@ export class RocketRideClient extends DAPClient {
|
|
|
1764
2135
|
* ```
|
|
1765
2136
|
*/
|
|
1766
2137
|
async getServices() {
|
|
1767
|
-
|
|
1768
|
-
const request = this.buildRequest('rrext_services', {});
|
|
1769
|
-
// Send to server and wait for response
|
|
1770
|
-
const response = await this.request(request);
|
|
1771
|
-
// Check if request failed
|
|
1772
|
-
if (this.didFail(response)) {
|
|
1773
|
-
const errorMsg = response.message || 'Failed to retrieve services';
|
|
1774
|
-
throw new Error(`Failed to retrieve services: ${errorMsg}`);
|
|
1775
|
-
}
|
|
1776
|
-
// Return the body containing all service definitions
|
|
1777
|
-
return response.body || {};
|
|
2138
|
+
return this.call('rrext_services', {});
|
|
1778
2139
|
}
|
|
1779
2140
|
/**
|
|
1780
2141
|
* Retrieve a specific service definition from the server.
|
|
@@ -1802,19 +2163,7 @@ export class RocketRideClient extends DAPClient {
|
|
|
1802
2163
|
if (!service) {
|
|
1803
2164
|
throw new Error('Service name is required');
|
|
1804
2165
|
}
|
|
1805
|
-
|
|
1806
|
-
const request = this.buildRequest('rrext_services', {
|
|
1807
|
-
arguments: { service }
|
|
1808
|
-
});
|
|
1809
|
-
// Send to server and wait for response
|
|
1810
|
-
const response = await this.request(request);
|
|
1811
|
-
// Check if request failed
|
|
1812
|
-
if (this.didFail(response)) {
|
|
1813
|
-
const errorMsg = response.message || `Service '${service}' not found`;
|
|
1814
|
-
throw new Error(`Failed to retrieve service '${service}': ${errorMsg}`);
|
|
1815
|
-
}
|
|
1816
|
-
// Return the body containing the service definition
|
|
1817
|
-
return response.body;
|
|
2166
|
+
return this.call('rrext_services', { service });
|
|
1818
2167
|
}
|
|
1819
2168
|
// ============================================================================
|
|
1820
2169
|
// ADDITIONAL CONVENIENCE METHODS
|
|
@@ -1835,6 +2184,109 @@ export class RocketRideClient extends DAPClient {
|
|
|
1835
2184
|
getApiKey() {
|
|
1836
2185
|
return this._apikey;
|
|
1837
2186
|
}
|
|
2187
|
+
// ============================================================================
|
|
2188
|
+
// ACCOUNT & BILLING NAMESPACES
|
|
2189
|
+
// ============================================================================
|
|
2190
|
+
/**
|
|
2191
|
+
* Lazily-initialised account API namespace.
|
|
2192
|
+
*
|
|
2193
|
+
* Provides typed methods for managing the authenticated user's profile,
|
|
2194
|
+
* API keys, organization, members, and teams.
|
|
2195
|
+
*
|
|
2196
|
+
* @example
|
|
2197
|
+
* ```typescript
|
|
2198
|
+
* const profile = await client.account.getProfile();
|
|
2199
|
+
* ```
|
|
2200
|
+
*/
|
|
2201
|
+
get account() {
|
|
2202
|
+
if (!this._account) {
|
|
2203
|
+
this._account = new AccountApi(this);
|
|
2204
|
+
}
|
|
2205
|
+
return this._account;
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Lazily-initialised billing API namespace.
|
|
2209
|
+
*
|
|
2210
|
+
* Provides typed methods for managing subscriptions, Stripe checkout
|
|
2211
|
+
* sessions, billing portal access, and compute credit wallets.
|
|
2212
|
+
*
|
|
2213
|
+
* @example
|
|
2214
|
+
* ```typescript
|
|
2215
|
+
* const details = await client.billing.getDetails(orgId);
|
|
2216
|
+
* ```
|
|
2217
|
+
*/
|
|
2218
|
+
get billing() {
|
|
2219
|
+
if (!this._billing) {
|
|
2220
|
+
this._billing = new BillingApi(this);
|
|
2221
|
+
}
|
|
2222
|
+
return this._billing;
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* Lazily-initialised database API namespace.
|
|
2226
|
+
*
|
|
2227
|
+
* Provides direct SQL/Cypher execution against database pipelines, bypassing
|
|
2228
|
+
* the LLM translation layer that {@link RocketRideClient.chat} uses.
|
|
2229
|
+
*
|
|
2230
|
+
* @example
|
|
2231
|
+
* ```typescript
|
|
2232
|
+
* const result = await client.database.query({ token, sql: 'SELECT 1' });
|
|
2233
|
+
* ```
|
|
2234
|
+
*/
|
|
2235
|
+
get database() {
|
|
2236
|
+
if (!this._database) {
|
|
2237
|
+
this._database = new DatabaseApi(this);
|
|
2238
|
+
}
|
|
2239
|
+
return this._database;
|
|
2240
|
+
}
|
|
2241
|
+
// ============================================================================
|
|
2242
|
+
// CALL — PUBLIC DAP COMMAND INTERFACE
|
|
2243
|
+
// ============================================================================
|
|
2244
|
+
/**
|
|
2245
|
+
* Sends a DAP command, unwraps the response body, and throws on failure.
|
|
2246
|
+
*
|
|
2247
|
+
* This is the single public entry point for all typed DAP operations.
|
|
2248
|
+
* The {@link AccountApi} and {@link BillingApi} namespaces delegate here.
|
|
2249
|
+
*
|
|
2250
|
+
* If an `onTrace` callback was provided in the constructor config, it is
|
|
2251
|
+
* invoked before the request (TraceType.Request) and after completion
|
|
2252
|
+
* (TraceType.Success or TraceType.Error).
|
|
2253
|
+
*
|
|
2254
|
+
* @param command - DAP command name (e.g. "rrext_account_me").
|
|
2255
|
+
* @param args - Key/value arguments forwarded in the request.
|
|
2256
|
+
* @param options - Optional token (for task-scoped calls) and timeout in ms.
|
|
2257
|
+
* @returns The `body` field of a successful DAP response.
|
|
2258
|
+
* @throws Error if the server signals failure.
|
|
2259
|
+
*/
|
|
2260
|
+
async call(command, args, options) {
|
|
2261
|
+
// Build the raw DAP request
|
|
2262
|
+
const message = this.buildRequest(command, {
|
|
2263
|
+
arguments: args,
|
|
2264
|
+
token: options?.token,
|
|
2265
|
+
});
|
|
2266
|
+
// Trace: outbound request
|
|
2267
|
+
this._onTrace?.(TraceType.Request, message);
|
|
2268
|
+
const response = await this.request(message, options?.timeout);
|
|
2269
|
+
// Throw on server-reported failure
|
|
2270
|
+
if (response.success === false) {
|
|
2271
|
+
this._onTrace?.(TraceType.Error, response);
|
|
2272
|
+
throw new Error(response.message ?? `${command} failed`);
|
|
2273
|
+
}
|
|
2274
|
+
// Trace: success response
|
|
2275
|
+
this._onTrace?.(TraceType.Success, response);
|
|
2276
|
+
// Unwrap the body envelope
|
|
2277
|
+
return (response.body ?? response);
|
|
2278
|
+
}
|
|
1838
2279
|
}
|
|
2280
|
+
// ============================================================================
|
|
2281
|
+
// PATH AND ID VALIDATION
|
|
2282
|
+
// ============================================================================
|
|
2283
|
+
/**
|
|
2284
|
+
* Characters that are illegal in store paths and IDs on all supported
|
|
2285
|
+
* platforms (Windows, Linux, macOS, and object-storage back-ends).
|
|
2286
|
+
*
|
|
2287
|
+
* `\x00` is the null byte; the rest are shell/filesystem metacharacters
|
|
2288
|
+
* that would cause ambiguous or dangerous behaviour in path construction.
|
|
2289
|
+
*/
|
|
2290
|
+
RocketRideClient.INVALID_PATH_CHARS = new Set(['*', '?', '<', '>', '|', '"', '\x00']);
|
|
1839
2291
|
export { RocketRideClient as default };
|
|
1840
2292
|
//# sourceMappingURL=client.js.map
|