@machhub-dev/sdk-ts 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sdk-ts.js CHANGED
@@ -15,8 +15,8 @@ class HTTPClient {
15
15
  * @param applicationID The ID for your application (required)
16
16
  * @param httpUrl The base URL for HTTP connection (default = http://localhost:6188)
17
17
  */
18
- constructor(applicationID, httpUrl, developerKey) {
19
- this.httpService = new HTTPService(httpUrl, MACHHUB_SDK_PATH, applicationID, developerKey);
18
+ constructor(applicationID, httpUrl, developerKey, runtimeID) {
19
+ this.httpService = new HTTPService(httpUrl, MACHHUB_SDK_PATH, applicationID, developerKey, runtimeID);
20
20
  }
21
21
  /**
22
22
  * Gets server info
@@ -144,20 +144,20 @@ export class SDK {
144
144
  if (!config.application_id)
145
145
  config = { application_id: "" };
146
146
  // console.log("Using application_id:", config.application_id);
147
- const PORT = await getEnvPort();
148
- // console.log("Using port:", PORT);
147
+ const { port, runtimeID } = await getEnvConfig();
148
+ console.log("Port:", port);
149
149
  if (!config.httpUrl) {
150
- config.httpUrl = "http://localhost:" + PORT;
150
+ config.httpUrl = "http://localhost:" + port;
151
151
  }
152
152
  if (!config.mqttUrl) {
153
- config.mqttUrl = "ws://localhost:" + PORT + "/mqtt";
153
+ config.mqttUrl = "ws://localhost:" + port + "/mqtt";
154
154
  }
155
155
  if (!config.natsUrl) {
156
- config.natsUrl = "ws://localhost:" + PORT + "/nats";
156
+ config.natsUrl = "ws://localhost:" + port + "/nats";
157
157
  }
158
158
  const { application_id, httpUrl, mqttUrl, natsUrl } = config;
159
- // console.log("Final config:", { application_id, httpUrl, mqttUrl, natsUrl });
160
- this.http = new HTTPClient(application_id, httpUrl, config.developer_key);
159
+ console.log("SDK Config:", { application_id, httpUrl, mqttUrl, natsUrl, developer_key: config.developer_key?.split('').map((_, i) => i < config.developer_key.length - 4 ? '*' : config.developer_key[i]).join('') });
160
+ this.http = new HTTPClient(application_id, httpUrl, config.developer_key, runtimeID);
161
161
  this.mqtt = await MQTTClient.getInstance(application_id, mqttUrl, config.developer_key);
162
162
  this.nats = await NATSClient.getInstance(application_id, natsUrl);
163
163
  this._historian = new Historian(this.http["httpService"], this.mqtt["mqttService"]);
@@ -230,27 +230,106 @@ export class SDK {
230
230
  return new Collection(this.http["httpService"], this.mqtt ? this.mqtt["mqttService"] : null, collectionName);
231
231
  }
232
232
  }
233
- async function getEnvPort() {
233
+ async function getEnvConfig() {
234
234
  try {
235
- const response = await fetchData(window.location.origin + "/_cfg");
235
+ // Try to find the configuration endpoint by testing different base paths
236
+ const configUrl = await findConfigEndpoint();
237
+ const response = await fetchData(configUrl);
236
238
  // console.log('Response:', response);
237
239
  // console.log('runtimeID: ', response.runtimeID);
238
240
  // console.log('applicationID: ', response.runtimeID.split('XmchX')[0]);
239
- return response.port;
241
+ return { port: response.port, runtimeID: response.runtimeID };
240
242
  }
241
243
  catch (error) {
242
244
  // console.log('No configured runtime ID:', error);
243
245
  // TODO: Use DevPort from SDK Config or default to 61888
244
- return "61888";
246
+ return { port: "61888", runtimeID: "" };
245
247
  }
246
248
  }
247
- async function fetchData(url) {
249
+ /**
250
+ * Attempts to find the correct configuration endpoint by trying different base paths
251
+ * Handles both port-based hosting (direct) and path-based hosting (reverse proxy)
252
+ */
253
+ async function findConfigEndpoint() {
254
+ const origin = window.location.origin;
255
+ const pathname = window.location.pathname;
256
+ // List of potential base paths to try, ordered by likelihood
257
+ const basePaths = [
258
+ // 1. Try origin directly (for port-based hosting like localhost:6190)
259
+ origin,
260
+ // 2. Try current path segments for path-based hosting
261
+ ...generatePathCandidates(pathname),
262
+ // 3. Try common root paths as fallback
263
+ origin,
264
+ ];
265
+ // Remove duplicates while preserving order
266
+ const uniqueBasePaths = [...new Set(basePaths)];
267
+ for (const basePath of uniqueBasePaths) {
268
+ try {
269
+ const configUrl = `${basePath}/_cfg`;
270
+ // Test if this endpoint returns valid JSON config by making a GET request
271
+ const testResponse = await fetch(configUrl, {
272
+ method: 'GET',
273
+ headers: {
274
+ 'Accept': 'application/json',
275
+ },
276
+ signal: AbortSignal.timeout(2000) // 2 second timeout
277
+ });
278
+ if (testResponse.ok) {
279
+ // Validate that the response is JSON and contains the expected 'port' field
280
+ const contentType = testResponse.headers.get('content-type');
281
+ if (contentType && contentType.includes('application/json')) {
282
+ try {
283
+ const testData = await testResponse.json();
284
+ // Check if the response has the expected structure with a 'port' field
285
+ if (testData && typeof testData === 'object' && 'port' in testData) {
286
+ // console.log(`Found config endpoint at: ${configUrl}`);
287
+ return configUrl;
288
+ }
289
+ }
290
+ catch (jsonError) {
291
+ // Not valid JSON, continue to next candidate
292
+ continue;
293
+ }
294
+ }
295
+ }
296
+ }
297
+ catch (error) {
298
+ // Continue to next candidate
299
+ continue;
300
+ }
301
+ }
302
+ // If all attempts fail, default to origin + /_cfg
303
+ console.warn('Could not find config endpoint, using default origin');
304
+ return `${origin}/_cfg`;
305
+ }
306
+ /**
307
+ * Generates potential base path candidates from the current pathname
308
+ * For example, /demo2/homepage/settings would generate:
309
+ * - http://localhost/demo2/homepage
310
+ * - http://localhost/demo2
311
+ * - http://localhost
312
+ */
313
+ function generatePathCandidates(pathname) {
314
+ const origin = window.location.origin;
315
+ const pathSegments = pathname.split('/').filter(segment => segment.length > 0);
316
+ const candidates = [];
317
+ // Generate paths by progressively removing segments from the end
318
+ for (let i = pathSegments.length; i > 0; i--) {
319
+ const path = '/' + pathSegments.slice(0, i).join('/');
320
+ candidates.push(origin + path);
321
+ }
322
+ return candidates;
323
+ }
324
+ async function fetchData(url, options) {
248
325
  try {
249
326
  const response = await fetch(url, {
250
327
  method: 'GET',
251
328
  headers: {
252
329
  'Accept': 'application/json',
253
330
  },
331
+ signal: AbortSignal.timeout(5000), // 5 second timeout
332
+ ...options
254
333
  });
255
334
  if (!response.ok) {
256
335
  throw new Error(`HTTP error! status: ${response.status}`);
@@ -9,19 +9,19 @@ export declare class HTTPService {
9
9
  private url;
10
10
  private applicationID;
11
11
  private developerKey?;
12
- constructor(url: string, prefix: string, applicationID: string, developerKey?: string);
12
+ private runtimeID?;
13
+ constructor(url: string, prefix: string, applicationID: string, developerKey?: string, runtimeID?: string);
13
14
  get request(): RequestParameters;
14
- private addRuntimeHeaders;
15
- private getCookie;
16
15
  }
17
16
  declare class RequestParameters {
18
17
  private base;
19
18
  private applicationID;
20
19
  private developerKey?;
20
+ private runtimeID?;
21
21
  query?: Record<string, string>;
22
22
  init?: RequestInit;
23
23
  headers?: Record<string, string>;
24
- constructor(base: URL, applicationID: string, developerKey?: string, query?: Record<string, string>);
24
+ constructor(base: URL, applicationID: string, developerKey?: string, runtimeID?: string, query?: Record<string, string>);
25
25
  private withQuery;
26
26
  private parseInit;
27
27
  withBody(body: BodyInit): RequestParameters;
@@ -33,6 +33,7 @@ declare class RequestParameters {
33
33
  setHeader(key: string, value: string): RequestParameters;
34
34
  setBearerToken(token: string): RequestParameters;
35
35
  withAccessToken(): RequestParameters;
36
+ withRuntimeID(): RequestParameters;
36
37
  withDomain(): RequestParameters;
37
38
  withDeveloperKey(): RequestParameters;
38
39
  withContentType(mime: string): RequestParameters;
@@ -10,46 +10,29 @@ export class HTTPException extends Error {
10
10
  }
11
11
  }
12
12
  export class HTTPService {
13
- constructor(url, prefix, applicationID, developerKey) {
13
+ constructor(url, prefix, applicationID, developerKey, runtimeID) {
14
14
  if (prefix == null)
15
15
  prefix = "";
16
16
  this.url = new URL(prefix, url);
17
17
  this.applicationID = "domains:" + applicationID;
18
18
  this.developerKey = developerKey;
19
+ this.runtimeID = runtimeID;
19
20
  }
20
21
  get request() {
21
- return new RequestParameters(this.url, this.applicationID, this.developerKey);
22
- }
23
- addRuntimeHeaders(headers = {}) {
24
- // Add runtime ID from cookie if available (for hosted applications)
25
- if (typeof document !== 'undefined') {
26
- const runtimeID = this.getCookie('machhub_runtime_id');
27
- if (runtimeID) {
28
- headers['X-MachHub-Runtime-ID'] = runtimeID;
29
- }
30
- }
31
- return headers;
32
- }
33
- getCookie(name) {
34
- if (typeof document === 'undefined')
35
- return null;
36
- const value = `; ${document.cookie}`;
37
- const parts = value.split(`; ${name}=`);
38
- if (parts.length === 2) {
39
- return parts.pop()?.split(';').shift() || null;
40
- }
41
- return null;
22
+ return new RequestParameters(this.url, this.applicationID, this.developerKey, this.runtimeID);
42
23
  }
43
24
  }
44
25
  class RequestParameters {
45
- constructor(base, applicationID, developerKey, query) {
26
+ constructor(base, applicationID, developerKey, runtimeID, query) {
46
27
  this.base = base;
47
28
  this.applicationID = applicationID;
48
29
  this.developerKey = developerKey;
30
+ this.runtimeID = runtimeID;
49
31
  this.query = query;
50
32
  this.withDomain(); // Ensure withDomain() is called by default
51
33
  this.withAccessToken(); // Ensure withAccessToken() is called by default
52
34
  this.withDeveloperKey(); // Ensure withDeveloperKey() is called by default
35
+ this.withRuntimeID(); // Ensure withRuntimeID() is called by default
53
36
  }
54
37
  withQuery(path, query) {
55
38
  const newPath = [this.base.pathname, path].map(pathPart => pathPart.replace(/(^\/|\/$)/g, "")).join("/");
@@ -133,6 +116,10 @@ class RequestParameters {
133
116
  this.setHeader("Authorization", `Bearer ${tkn}`);
134
117
  return this;
135
118
  }
119
+ withRuntimeID() {
120
+ this.setHeader("X-Machhub-Runtime-Id", this.runtimeID ?? "");
121
+ return this;
122
+ }
136
123
  withDomain() {
137
124
  this.setHeader("Domain", this.applicationID);
138
125
  return this;
@@ -163,6 +150,12 @@ class RequestParameters {
163
150
  if (body) {
164
151
  if (body instanceof FormData) {
165
152
  init.body = body;
153
+ // Remove Content-Type header if it exists, let browser set it for FormData
154
+ if (init.headers && typeof init.headers === 'object') {
155
+ const headers = init.headers;
156
+ delete headers['Content-Type'];
157
+ delete headers['content-type'];
158
+ }
166
159
  }
167
160
  else {
168
161
  init.body = JSON.stringify(body);
@@ -36,7 +36,7 @@ export class MQTTService {
36
36
  this.subscribedTopics.push({ topic, handler });
37
37
  if (topic == "")
38
38
  return;
39
- console.log("New Subscription Handler:", topic);
39
+ // console.log("New Subscription Handler:", topic);
40
40
  this.client.subscribe(topic, { qos: 2 }, (err) => {
41
41
  if (err) {
42
42
  console.error(`Failed to subscribe to topic ${topic}:`, err);
@@ -55,7 +55,7 @@ export class MQTTService {
55
55
  publish(topic, message) {
56
56
  try {
57
57
  const payload = JSON.stringify(message);
58
- console.log("Publishing to", topic, "with payload:", payload);
58
+ // console.log("Publishing to", topic, "with payload:", payload);
59
59
  this.client.publish(topic, payload, {
60
60
  qos: 2,
61
61
  retain: true,
@@ -66,9 +66,9 @@ export declare class NATSService {
66
66
  */
67
67
  printFunctions(): void;
68
68
  /**
69
- * Parses a message buffer into a JSON object.
69
+ * Parses a message buffer into a JSON object or returns raw data.
70
70
  * @param message {Uint8Array} The message buffer.
71
- * @returns {unknown} The parsed message.
71
+ * @returns {unknown} The parsed message (JSON object) or the raw string if parsing fails.
72
72
  */
73
73
  private parseMessage;
74
74
  /**
@@ -104,20 +104,31 @@ export class NATSService {
104
104
  console.error("Error handling message:", err);
105
105
  return;
106
106
  }
107
- const data = JSON.parse(msg.data.toString());
108
- const subjectParts = msg.subject.split(".");
109
- if (subjectParts.length !== 4) {
110
- msg.respond(JSON.stringify({ error: "Invalid subject format" }));
111
- return;
107
+ try {
108
+ const dataStr = Buffer.from(msg.data).toString('utf-8');
109
+ const data = JSON.parse(dataStr);
110
+ const subjectParts = msg.subject.split(".");
111
+ if (subjectParts.length !== 4) {
112
+ msg.respond(JSON.stringify({ error: "Invalid subject format" }));
113
+ return;
114
+ }
115
+ const functionName = subjectParts[3];
116
+ this.executeFunction(functionName, data)
117
+ .then((result) => {
118
+ msg.respond(JSON.stringify(result));
119
+ })
120
+ .catch((e) => {
121
+ console.log("Error executing function '" + functionName + "': ", e);
122
+ msg.respond(JSON.stringify({ status: "failed", error: e.message }));
123
+ });
124
+ }
125
+ catch (parseError) {
126
+ console.error("Error parsing function execution message:", parseError);
127
+ msg.respond(JSON.stringify({
128
+ status: "failed",
129
+ error: `Failed to parse message: ${parseError.message}`
130
+ }));
112
131
  }
113
- const functionName = subjectParts[3];
114
- this.executeFunction(functionName, data)
115
- .then((result) => {
116
- msg.respond(JSON.stringify(result));
117
- })
118
- .catch((e) => {
119
- msg.respond(JSON.stringify({ status: "failed", error: e.message }));
120
- });
121
132
  },
122
133
  });
123
134
  if (sub)
@@ -192,17 +203,34 @@ export class NATSService {
192
203
  NATSService.log("Available functions: ", Object.keys(this.functions).join(", "));
193
204
  }
194
205
  /**
195
- * Parses a message buffer into a JSON object.
206
+ * Parses a message buffer into a JSON object or returns raw data.
196
207
  * @param message {Uint8Array} The message buffer.
197
- * @returns {unknown} The parsed message.
208
+ * @returns {unknown} The parsed message (JSON object) or the raw string if parsing fails.
198
209
  */
199
210
  parseMessage(message) {
200
211
  try {
201
- return JSON.parse(Buffer.from(message).toString());
212
+ const messageStr = Buffer.from(message).toString('utf-8');
213
+ // Check if the message is empty
214
+ if (!messageStr || messageStr.trim().length === 0) {
215
+ NATSService.log("Received empty message");
216
+ return null;
217
+ }
218
+ // Try to parse as JSON
219
+ return JSON.parse(messageStr);
202
220
  }
203
221
  catch (error) {
222
+ // If JSON parsing fails, check if it's binary data or non-JSON content
223
+ const messageStr = Buffer.from(message).toString('utf-8');
224
+ // Check if it looks like binary data (contains non-printable characters)
225
+ const isBinary = message.some(byte => byte < 32 && byte !== 9 && byte !== 10 && byte !== 13);
226
+ if (isBinary) {
227
+ NATSService.log("Received binary data, returning as Uint8Array");
228
+ return message; // Return raw binary data
229
+ }
230
+ // If it's a plain string (not JSON), return as is
231
+ NATSService.log("Failed to parse message as JSON, returning as string:", messageStr.substring(0, 100));
204
232
  console.error("Error parsing message:", error);
205
- return null;
233
+ return messageStr;
206
234
  }
207
235
  }
208
236
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@machhub-dev/sdk-ts",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "MACHHUB TYPESCRIPT SDK",
5
5
  "keywords": [
6
6
  "machhub",
@@ -20,14 +20,14 @@
20
20
  "build": "npx tsc && npx tsc --module CommonJS --outDir dist/cjs"
21
21
  },
22
22
  "dependencies": {
23
+ "@nats-io/nats-core": "^3.0.2",
24
+ "@nats-io/transport-node": "^3.0.2",
23
25
  "mqtt": "^5.10.4",
24
26
  "safe-buffer": "^5.2.1",
25
27
  "typescript": "^5.8.3",
26
- "undici-types": "^7.4.0",
27
- "@nats-io/nats-core": "^3.0.2",
28
- "@nats-io/transport-node": "^3.0.2"
28
+ "undici-types": "^7.4.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.13.5"
32
32
  }
33
- }
33
+ }
@@ -1,6 +1,20 @@
1
1
  import { HTTPService } from "../services/http.service.js";
2
2
  import { MQTTService } from "../services/mqtt.service.js";
3
3
 
4
+ export class CollectionError extends Error {
5
+ public operation: string;
6
+ public collectionName: string;
7
+ public originalError: Error;
8
+
9
+ constructor(operation: string, collectionName: string, originalError: Error) {
10
+ super(`Collection operation '${operation}' failed on '${collectionName}': ${originalError.message}`);
11
+ this.name = 'CollectionError';
12
+ this.operation = operation;
13
+ this.collectionName = collectionName;
14
+ this.originalError = originalError;
15
+ }
16
+ }
17
+
4
18
  export class Collection {
5
19
  protected httpService: HTTPService;
6
20
  protected mqttService: MQTTService | null;
@@ -13,6 +27,7 @@ export class Collection {
13
27
  this.collectionName = collectionName;
14
28
  }
15
29
 
30
+
16
31
  filter(fieldName: string, operator: "=" | ">" | "<" | "<=" | ">=" | "!=", value: any): Collection {
17
32
  this.queryParams[`filter[${fieldName}][${operator}][${typeof value}]`] = value;
18
33
  return this;
@@ -34,31 +49,62 @@ export class Collection {
34
49
  }
35
50
 
36
51
  async getAll(): Promise<any[]> {
37
- return this.httpService.request.get(this.collectionName + "/all", this.queryParams);
52
+ try {
53
+ return await this.httpService.request.get(this.collectionName + "/all", this.queryParams);
54
+ } catch (error) {
55
+ throw new CollectionError('getAll', this.collectionName, error as Error);
56
+ }
38
57
  }
39
58
 
40
59
  async getOne(id: string): Promise<any> {
41
60
  if (!id) {
42
61
  throw new Error("ID must be provided");
43
62
  }
44
- return this.httpService.request.get(id);
63
+ try {
64
+ return await this.httpService.request.get(id);
65
+ } catch (error) {
66
+ throw new CollectionError('getOne', this.collectionName, error as Error);
67
+ }
45
68
  }
46
69
 
47
70
  async create(data: Record<string, any>): Promise<any> {
48
- return this.httpService.request.withJSON(data).post(this.collectionName);
71
+ try {
72
+ const formData = new FormData();
73
+
74
+ for (const [key, value] of Object.entries(data)) {
75
+ if (value instanceof File) {
76
+ formData.append(key, value, value.name);
77
+ data[key] = value.name
78
+ }
79
+ }
80
+ formData.append("record", JSON.stringify(data))
81
+ return await this.httpService.request
82
+ .withBody(formData)
83
+ .post(this.collectionName);
84
+ } catch (error) {
85
+ throw new CollectionError("create", this.collectionName, error as Error);
86
+ }
49
87
  }
50
88
 
51
89
  async update(id: string, data: Record<string, any>): Promise<any> {
52
90
  if (!id) {
53
91
  throw new Error("ID must be provided");
54
92
  }
55
- return this.httpService.request.withJSON(data).put(id);
93
+ try {
94
+ return await this.httpService.request.withJSON(data).put(id);
95
+ } catch (error) {
96
+ throw new CollectionError('update', this.collectionName, error as Error);
97
+ }
56
98
  }
57
99
 
58
100
  async delete(id: string): Promise<any> {
59
101
  if (!id) {
60
102
  throw new Error("ID must be provided");
61
103
  }
62
- return this.httpService.request.delete(id);
104
+ try {
105
+ return await this.httpService.request.delete(id);
106
+ } catch (error) {
107
+ throw new CollectionError('delete', this.collectionName, error as Error);
108
+ }
63
109
  }
64
110
  }
package/src/sdk-ts.ts CHANGED
@@ -19,8 +19,8 @@ class HTTPClient {
19
19
  * @param applicationID The ID for your application (required)
20
20
  * @param httpUrl The base URL for HTTP connection (default = http://localhost:6188)
21
21
  */
22
- constructor(applicationID: string, httpUrl:string, developerKey?: string) {
23
- this.httpService = new HTTPService(httpUrl, MACHHUB_SDK_PATH, applicationID, developerKey);
22
+ constructor(applicationID: string, httpUrl:string, developerKey?: string, runtimeID?: string) {
23
+ this.httpService = new HTTPService(httpUrl, MACHHUB_SDK_PATH, applicationID, developerKey, runtimeID);
24
24
  }
25
25
 
26
26
  /**
@@ -173,26 +173,26 @@ export class SDK {
173
173
  // console.log("Using application_id:", config.application_id);
174
174
 
175
175
 
176
- const PORT = await getEnvPort();
177
- // console.log("Using port:", PORT);
176
+ const {port, runtimeID} = await getEnvConfig();
177
+ console.log("Port:", port);
178
178
 
179
179
  if (!config.httpUrl) {
180
- config.httpUrl = "http://localhost:" + PORT;
180
+ config.httpUrl = "http://localhost:" + port;
181
181
  }
182
182
 
183
183
  if (!config.mqttUrl) {
184
- config.mqttUrl = "ws://localhost:" + PORT + "/mqtt";
184
+ config.mqttUrl = "ws://localhost:" + port + "/mqtt";
185
185
  }
186
186
 
187
187
  if (!config.natsUrl) {
188
- config.natsUrl = "ws://localhost:" + PORT + "/nats";
188
+ config.natsUrl = "ws://localhost:" + port + "/nats";
189
189
  }
190
190
 
191
191
  const { application_id, httpUrl, mqttUrl, natsUrl } = config;
192
192
 
193
- // console.log("Final config:", { application_id, httpUrl, mqttUrl, natsUrl });
193
+ console.log("SDK Config:", { application_id, httpUrl, mqttUrl, natsUrl, developer_key: config.developer_key?.split('').map((_, i) => i < config!.developer_key!.length - 4 ? '*' : config!.developer_key![i]).join('') });
194
194
 
195
- this.http = new HTTPClient(application_id, httpUrl, config.developer_key);
195
+ this.http = new HTTPClient(application_id, httpUrl, config.developer_key, runtimeID);
196
196
  this.mqtt = await MQTTClient.getInstance(application_id, mqttUrl, config.developer_key);
197
197
  this.nats = await NATSClient.getInstance(application_id, natsUrl);
198
198
 
@@ -273,28 +273,117 @@ export class SDK {
273
273
  }
274
274
  }
275
275
 
276
- async function getEnvPort(): Promise<string> {
276
+ async function getEnvConfig(): Promise<{port:string, runtimeID:string}> {
277
277
  try {
278
- const response = await fetchData<{runtimeID:string, port:string}>(window.location.origin + "/_cfg");
278
+ // Try to find the configuration endpoint by testing different base paths
279
+ const configUrl = await findConfigEndpoint();
280
+ const response = await fetchData<{port:string, runtimeID:string}>(configUrl);
279
281
  // console.log('Response:', response);
280
282
  // console.log('runtimeID: ', response.runtimeID);
281
283
  // console.log('applicationID: ', response.runtimeID.split('XmchX')[0]);
282
- return response.port;
284
+ return { port: response.port, runtimeID: response.runtimeID};
283
285
  } catch (error) {
284
286
  // console.log('No configured runtime ID:', error);
285
287
  // TODO: Use DevPort from SDK Config or default to 61888
286
- return "61888";
288
+ return { port: "61888", runtimeID: ""};
287
289
  }
288
290
  }
289
291
 
292
+ /**
293
+ * Attempts to find the correct configuration endpoint by trying different base paths
294
+ * Handles both port-based hosting (direct) and path-based hosting (reverse proxy)
295
+ */
296
+ async function findConfigEndpoint(): Promise<string> {
297
+ const origin = window.location.origin;
298
+ const pathname = window.location.pathname;
299
+
300
+ // List of potential base paths to try, ordered by likelihood
301
+ const basePaths = [
302
+ // 1. Try origin directly (for port-based hosting like localhost:6190)
303
+ origin,
304
+
305
+ // 2. Try current path segments for path-based hosting
306
+ ...generatePathCandidates(pathname),
307
+
308
+ // 3. Try common root paths as fallback
309
+ origin,
310
+ ];
311
+
312
+ // Remove duplicates while preserving order
313
+ const uniqueBasePaths = [...new Set(basePaths)];
314
+
315
+ for (const basePath of uniqueBasePaths) {
316
+ try {
317
+ const configUrl = `${basePath}/_cfg`;
318
+
319
+ // Test if this endpoint returns valid JSON config by making a GET request
320
+ const testResponse = await fetch(configUrl, {
321
+ method: 'GET',
322
+ headers: {
323
+ 'Accept': 'application/json',
324
+ },
325
+ signal: AbortSignal.timeout(2000) // 2 second timeout
326
+ });
327
+
328
+ if (testResponse.ok) {
329
+ // Validate that the response is JSON and contains the expected 'port' field
330
+ const contentType = testResponse.headers.get('content-type');
331
+ if (contentType && contentType.includes('application/json')) {
332
+ try {
333
+ const testData = await testResponse.json();
334
+ // Check if the response has the expected structure with a 'port' field
335
+ if (testData && typeof testData === 'object' && 'port' in testData) {
336
+ // console.log(`Found config endpoint at: ${configUrl}`);
337
+ return configUrl;
338
+ }
339
+ } catch (jsonError) {
340
+ // Not valid JSON, continue to next candidate
341
+ continue;
342
+ }
343
+ }
344
+ }
345
+ } catch (error) {
346
+ // Continue to next candidate
347
+ continue;
348
+ }
349
+ }
350
+
351
+ // If all attempts fail, default to origin + /_cfg
352
+ console.warn('Could not find config endpoint, using default origin');
353
+ return `${origin}/_cfg`;
354
+ }
355
+
356
+ /**
357
+ * Generates potential base path candidates from the current pathname
358
+ * For example, /demo2/homepage/settings would generate:
359
+ * - http://localhost/demo2/homepage
360
+ * - http://localhost/demo2
361
+ * - http://localhost
362
+ */
363
+ function generatePathCandidates(pathname: string): string[] {
364
+ const origin = window.location.origin;
365
+ const pathSegments = pathname.split('/').filter(segment => segment.length > 0);
366
+ const candidates: string[] = [];
367
+
368
+ // Generate paths by progressively removing segments from the end
369
+ for (let i = pathSegments.length; i > 0; i--) {
370
+ const path = '/' + pathSegments.slice(0, i).join('/');
371
+ candidates.push(origin + path);
372
+ }
373
+
374
+ return candidates;
375
+ }
376
+
290
377
 
291
- async function fetchData<T>(url: string): Promise<T> {
378
+ async function fetchData<T>(url: string, options?: RequestInit): Promise<T> {
292
379
  try {
293
380
  const response = await fetch(url, {
294
381
  method: 'GET',
295
382
  headers: {
296
383
  'Accept': 'application/json',
297
384
  },
385
+ signal: AbortSignal.timeout(5000), // 5 second timeout
386
+ ...options
298
387
  });
299
388
 
300
389
  if (!response.ok) {