@iskra-bun/db-oracle 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @iskra-bun/db-oracle
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f9654df: Reject all pending requests when the Oracle bridge reports a fatal error or exits, instead of leaving their promises unsettled (which previously caused silent hangs and a memory leak).
8
+ - Updated dependencies [f9654df]
9
+ - Updated dependencies
10
+ - Updated dependencies [f9654df]
11
+ - @iskra-bun/core@0.1.1
12
+
3
13
  ## 0.1.0
4
14
 
5
15
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -6,12 +6,18 @@ declare class OracleDriver implements Driver {
6
6
  private reqId;
7
7
  private pending;
8
8
  private bridgePath;
9
- constructor(bridgePath?: string);
9
+ private timeoutMs;
10
+ private app;
11
+ constructor(bridgePath?: string, timeoutMs?: number);
10
12
  init(app: App): Promise<void>;
11
13
  start(app?: App): Promise<void>;
12
14
  stop(): Promise<void>;
15
+ private log;
16
+ private settle;
17
+ private rejectAllPending;
13
18
  query(sql: string, params?: any[]): Promise<unknown>;
14
19
  private readStream;
20
+ private handleLine;
15
21
  }
16
22
 
17
23
  export { OracleDriver };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/driver.ts
2
2
  import { spawn } from "bun";
3
3
  import { resolve } from "path";
4
+ var DEFAULT_TIMEOUT_MS = 3e4;
4
5
  var OracleDriver = class {
5
6
  name = "db";
6
7
  // Replaces standard db driver if used, or can be 'oracle'
@@ -8,15 +9,20 @@ var OracleDriver = class {
8
9
  reqId = 0;
9
10
  pending = /* @__PURE__ */ new Map();
10
11
  bridgePath;
11
- constructor(bridgePath) {
12
+ timeoutMs;
13
+ app = null;
14
+ constructor(bridgePath, timeoutMs = DEFAULT_TIMEOUT_MS) {
12
15
  this.bridgePath = bridgePath || resolve(import.meta.dir, "../bridge/runner.js");
16
+ this.timeoutMs = timeoutMs;
13
17
  }
14
18
  async init(app) {
19
+ this.app = app;
15
20
  app.context.set("oracle", this);
16
21
  }
17
22
  async start(app) {
23
+ if (app) this.app = app;
18
24
  if (!process.env.ORA_CONN) {
19
- console.warn("Oracle connection string (ORA_CONN) not set. Oracle driver will not start.");
25
+ this.log("warn", "Oracle connection string (ORA_CONN) not set. Oracle driver will not start.");
20
26
  return;
21
27
  }
22
28
  this.proc = spawn(["node", this.bridgePath], {
@@ -35,18 +41,52 @@ var OracleDriver = class {
35
41
  this.proc = null;
36
42
  }
37
43
  }
44
+ log(level, objOrMsg, msg) {
45
+ if (!this.app) return;
46
+ const logger = this.app.logger;
47
+ if (typeof objOrMsg === "string") {
48
+ logger[level](objOrMsg);
49
+ } else {
50
+ logger[level](objOrMsg, msg);
51
+ }
52
+ }
53
+ settle(id, action) {
54
+ const entry = this.pending.get(id);
55
+ if (!entry) return;
56
+ if (entry.timer) clearTimeout(entry.timer);
57
+ this.pending.delete(id);
58
+ action(entry);
59
+ }
60
+ rejectAllPending(err) {
61
+ if (this.pending.size === 0) return;
62
+ const ids = Array.from(this.pending.keys());
63
+ for (const id of ids) {
64
+ this.settle(id, ({ reject }) => reject(err));
65
+ }
66
+ }
38
67
  async query(sql, params = []) {
39
68
  if (!this.proc || !this.proc.stdin) {
40
69
  throw new Error("Oracle driver not started");
41
70
  }
42
71
  const id = this.reqId++;
43
72
  return new Promise((resolve2, reject) => {
44
- this.pending.set(id, { resolve: resolve2, reject });
73
+ const timer = setTimeout(() => {
74
+ this.settle(
75
+ id,
76
+ ({ reject: rej }) => rej(new Error(`Oracle query timed out after ${this.timeoutMs}ms`))
77
+ );
78
+ }, this.timeoutMs);
79
+ this.pending.set(id, { resolve: resolve2, reject, timer });
45
80
  const msg = JSON.stringify({ id, sql, params }) + "\n";
46
81
  const stdin = this.proc.stdin;
47
- if (stdin.write) {
82
+ if (typeof stdin.write === "function") {
48
83
  stdin.write(msg);
49
84
  stdin.flush();
85
+ } else {
86
+ this.settle(
87
+ id,
88
+ ({ reject: rej }) => rej(new Error("Oracle bridge stdin is not writable"))
89
+ );
50
90
  }
51
91
  });
52
92
  }
@@ -62,34 +102,40 @@ var OracleDriver = class {
62
102
  const lines = buffer.split("\n");
63
103
  buffer = lines.pop() || "";
64
104
  for (const line of lines) {
65
- if (!line.trim()) continue;
66
- try {
67
- const msg = JSON.parse(line);
68
- if (msg.type === "ready") {
69
- continue;
70
- }
71
- if (msg.type === "fatal") {
72
- console.error("Oracle Bridge Fatal Error:", msg.error);
73
- continue;
74
- }
75
- if (msg.id !== void 0 && this.pending.has(msg.id)) {
76
- const { resolve: resolve2, reject } = this.pending.get(msg.id);
77
- this.pending.delete(msg.id);
78
- if (msg.error) {
79
- reject(new Error(msg.error));
80
- } else {
81
- resolve2(msg.data);
82
- }
83
- }
84
- } catch (err) {
85
- console.error("Error parsing bridge message:", err, line);
86
- }
105
+ this.handleLine(line);
87
106
  }
88
107
  }
89
108
  } catch (err) {
90
- console.error("Error reading from Oracle bridge:", err);
109
+ this.log("error", { err }, "Error reading from Oracle bridge");
110
+ this.rejectAllPending(new Error("Oracle bridge stream error"));
91
111
  } finally {
92
112
  reader.releaseLock();
113
+ this.rejectAllPending(new Error("Oracle bridge process exited"));
114
+ }
115
+ }
116
+ handleLine(line) {
117
+ if (!line.trim()) return;
118
+ try {
119
+ const msg = JSON.parse(line);
120
+ if (msg.type === "ready") {
121
+ return;
122
+ }
123
+ if (msg.type === "fatal") {
124
+ this.log("error", { error: msg.error }, "Oracle Bridge Fatal Error");
125
+ this.rejectAllPending(new Error(`Oracle bridge fatal: ${msg.error}`));
126
+ return;
127
+ }
128
+ if (msg.id !== void 0 && this.pending.has(msg.id)) {
129
+ this.settle(msg.id, ({ resolve: resolve2, reject }) => {
130
+ if (msg.error) {
131
+ reject(new Error(msg.error));
132
+ } else {
133
+ resolve2(msg.data);
134
+ }
135
+ });
136
+ }
137
+ } catch (err) {
138
+ this.log("error", { err, line }, "Error parsing bridge message");
93
139
  }
94
140
  }
95
141
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/driver.ts"],"sourcesContent":["import type { Driver, App } from '@iskra-bun/core';\nimport { spawn, type Subprocess } from 'bun';\nimport { resolve } from 'path';\n\nexport class OracleDriver implements Driver {\n name = 'db'; // Replaces standard db driver if used, or can be 'oracle'\n private proc: Subprocess | null = null;\n private reqId = 0;\n private pending = new Map<number, { resolve: (val: any) => void, reject: (err: any) => void }>();\n private bridgePath: string;\n\n constructor(bridgePath?: string) {\n // Allow overriding path for flexibility (absolute path)\n // Defaults to calculating relative to this file in a built package structure\n // Adjust logic if needed based on where this file ends up (dist vs src)\n // For dev (src), it's ../bridge/runner.js\n this.bridgePath = bridgePath || resolve(import.meta.dir, '../bridge/runner.js');\n }\n\n async init(app: App) {\n // If we want to replace the main 'db' object or sit alongside it\n app.context.set('oracle', this);\n }\n\n async start(app?: App) { // app optional to satisfy interface but we might need config from it\n // Check env vars\n if (!process.env.ORA_CONN) {\n console.warn('Oracle connection string (ORA_CONN) not set. Oracle driver will not start.');\n return;\n }\n\n this.proc = spawn(['node', this.bridgePath], {\n stdin: 'pipe',\n stdout: 'pipe',\n env: { ...process.env },\n });\n\n if (!this.proc.stdout) {\n throw new Error('Failed to spawn Oracle bridge process (no stdout)');\n }\n\n this.readStream(this.proc.stdout as ReadableStream);\n\n // Wait for ready signal?\n // For now we assume optimistic start or we could wait for 'ready' message\n }\n\n async stop() {\n if (this.proc) {\n this.proc.kill();\n this.proc = null;\n }\n }\n\n async query(sql: string, params: any[] = []) {\n if (!this.proc || !this.proc.stdin) {\n throw new Error('Oracle driver not started');\n }\n\n const id = this.reqId++;\n\n return new Promise((resolve, reject) => {\n this.pending.set(id, { resolve, reject });\n\n const msg = JSON.stringify({ id, sql, params }) + '\\n';\n const stdin = this.proc!.stdin as any; // Bun FileSink/writer\n if (stdin.write) {\n stdin.write(msg);\n stdin.flush();\n }\n });\n }\n\n private async readStream(stream: ReadableStream) {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || ''; // Keep incomplete line\n\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const msg = JSON.parse(line);\n\n if (msg.type === 'ready') {\n // console.log('Oracle Bridge Ready');\n continue;\n }\n if (msg.type === 'fatal') {\n console.error('Oracle Bridge Fatal Error:', msg.error);\n // Reject all pending?\n continue;\n }\n\n if (msg.id !== undefined && this.pending.has(msg.id)) {\n const { resolve, reject } = this.pending.get(msg.id)!;\n this.pending.delete(msg.id);\n\n if (msg.error) {\n reject(new Error(msg.error));\n } else {\n resolve(msg.data);\n }\n }\n } catch (err) {\n console.error('Error parsing bridge message:', err, line);\n }\n }\n }\n } catch (err) {\n console.error('Error reading from Oracle bridge:', err);\n } finally {\n reader.releaseLock();\n }\n }\n}\n"],"mappings":"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","names":["resolve"]}
1
+ {"version":3,"sources":["../src/driver.ts"],"sourcesContent":["import type { Driver, App } from '@iskra-bun/core';\nimport { spawn, type Subprocess, type FileSink } from 'bun';\nimport { resolve } from 'path';\n\ntype PendingEntry = {\n resolve: (val: any) => void;\n reject: (err: any) => void;\n timer: ReturnType<typeof setTimeout> | null;\n};\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport class OracleDriver implements Driver {\n name = 'db'; // Replaces standard db driver if used, or can be 'oracle'\n private proc: Subprocess | null = null;\n private reqId = 0;\n private pending = new Map<number, PendingEntry>();\n private bridgePath: string;\n private timeoutMs: number;\n private app: App | null = null;\n\n constructor(bridgePath?: string, timeoutMs: number = DEFAULT_TIMEOUT_MS) {\n // Allow overriding path for flexibility (absolute path)\n // Defaults to calculating relative to this file in a built package structure\n // Adjust logic if needed based on where this file ends up (dist vs src)\n // For dev (src), it's ../bridge/runner.js\n this.bridgePath = bridgePath || resolve(import.meta.dir, '../bridge/runner.js');\n this.timeoutMs = timeoutMs;\n }\n\n async init(app: App) {\n this.app = app;\n // If we want to replace the main 'db' object or sit alongside it\n app.context.set('oracle', this);\n }\n\n async start(app?: App) { // app optional to satisfy interface but we might need config from it\n if (app) this.app = app;\n\n // Check env vars\n if (!process.env.ORA_CONN) {\n this.log('warn', 'Oracle connection string (ORA_CONN) not set. Oracle driver will not start.');\n return;\n }\n\n this.proc = spawn(['node', this.bridgePath], {\n stdin: 'pipe',\n stdout: 'pipe',\n env: { ...process.env },\n });\n\n if (!this.proc.stdout) {\n throw new Error('Failed to spawn Oracle bridge process (no stdout)');\n }\n\n this.readStream(this.proc.stdout as ReadableStream);\n\n // Wait for ready signal?\n // For now we assume optimistic start or we could wait for 'ready' message\n }\n\n async stop() {\n if (this.proc) {\n this.proc.kill();\n this.proc = null;\n }\n }\n\n private log(level: 'error' | 'warn', objOrMsg: unknown, msg?: string) {\n // Route all diagnostics through app.logger; never use the global console.\n if (!this.app) return;\n const logger = this.app.logger;\n if (typeof objOrMsg === 'string') {\n logger[level](objOrMsg);\n } else {\n logger[level](objOrMsg as object, msg);\n }\n }\n\n private settle(id: number, action: (entry: PendingEntry) => void) {\n const entry = this.pending.get(id);\n if (!entry) return;\n if (entry.timer) clearTimeout(entry.timer);\n this.pending.delete(id);\n action(entry);\n }\n\n private rejectAllPending(err: Error) {\n if (this.pending.size === 0) return;\n const ids = Array.from(this.pending.keys());\n for (const id of ids) {\n this.settle(id, ({ reject }) => reject(err));\n }\n }\n\n async query(sql: string, params: any[] = []) {\n if (!this.proc || !this.proc.stdin) {\n throw new Error('Oracle driver not started');\n }\n\n const id = this.reqId++;\n\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n this.settle(id, ({ reject: rej }) =>\n rej(new Error(`Oracle query timed out after ${this.timeoutMs}ms`)),\n );\n }, this.timeoutMs);\n\n this.pending.set(id, { resolve, reject, timer });\n\n const msg = JSON.stringify({ id, sql, params }) + '\\n';\n const stdin = this.proc!.stdin as FileSink;\n if (typeof stdin.write === 'function') {\n stdin.write(msg);\n stdin.flush();\n } else {\n // No writable stdin: do not leave the request hanging in pending.\n this.settle(id, ({ reject: rej }) =>\n rej(new Error('Oracle bridge stdin is not writable')),\n );\n }\n });\n }\n\n private async readStream(stream: ReadableStream) {\n const reader = stream.getReader();\n const decoder = new TextDecoder();\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() || ''; // Keep incomplete line\n\n for (const line of lines) {\n this.handleLine(line);\n }\n }\n } catch (err) {\n this.log('error', { err }, 'Error reading from Oracle bridge');\n this.rejectAllPending(new Error('Oracle bridge stream error'));\n } finally {\n reader.releaseLock();\n this.rejectAllPending(new Error('Oracle bridge process exited'));\n }\n }\n\n private handleLine(line: string) {\n if (!line.trim()) return;\n try {\n const msg = JSON.parse(line);\n\n if (msg.type === 'ready') {\n return;\n }\n if (msg.type === 'fatal') {\n this.log('error', { error: msg.error }, 'Oracle Bridge Fatal Error');\n this.rejectAllPending(new Error(`Oracle bridge fatal: ${msg.error}`));\n return;\n }\n\n if (msg.id !== undefined && this.pending.has(msg.id)) {\n this.settle(msg.id, ({ resolve, reject }) => {\n if (msg.error) {\n reject(new Error(msg.error));\n } else {\n resolve(msg.data);\n }\n });\n }\n } catch (err) {\n this.log('error', { err, line }, 'Error parsing bridge message');\n }\n }\n}\n"],"mappings":"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","names":["resolve"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iskra-bun/db-oracle",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Soporte para Oracle Database en Iskra (via puente/sidecar).",
5
5
  "keywords": [
6
6
  "iskra",
@@ -47,7 +47,7 @@
47
47
  "build": "tsup --config ../../tsup.config.ts"
48
48
  },
49
49
  "dependencies": {
50
- "@iskra-bun/core": "0.1.0"
50
+ "@iskra-bun/core": "0.1.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^22.10.2",
package/src/driver.ts CHANGED
@@ -1,31 +1,45 @@
1
1
  import type { Driver, App } from '@iskra-bun/core';
2
- import { spawn, type Subprocess } from 'bun';
2
+ import { spawn, type Subprocess, type FileSink } from 'bun';
3
3
  import { resolve } from 'path';
4
4
 
5
+ type PendingEntry = {
6
+ resolve: (val: any) => void;
7
+ reject: (err: any) => void;
8
+ timer: ReturnType<typeof setTimeout> | null;
9
+ };
10
+
11
+ const DEFAULT_TIMEOUT_MS = 30_000;
12
+
5
13
  export class OracleDriver implements Driver {
6
14
  name = 'db'; // Replaces standard db driver if used, or can be 'oracle'
7
15
  private proc: Subprocess | null = null;
8
16
  private reqId = 0;
9
- private pending = new Map<number, { resolve: (val: any) => void, reject: (err: any) => void }>();
17
+ private pending = new Map<number, PendingEntry>();
10
18
  private bridgePath: string;
19
+ private timeoutMs: number;
20
+ private app: App | null = null;
11
21
 
12
- constructor(bridgePath?: string) {
22
+ constructor(bridgePath?: string, timeoutMs: number = DEFAULT_TIMEOUT_MS) {
13
23
  // Allow overriding path for flexibility (absolute path)
14
24
  // Defaults to calculating relative to this file in a built package structure
15
25
  // Adjust logic if needed based on where this file ends up (dist vs src)
16
26
  // For dev (src), it's ../bridge/runner.js
17
27
  this.bridgePath = bridgePath || resolve(import.meta.dir, '../bridge/runner.js');
28
+ this.timeoutMs = timeoutMs;
18
29
  }
19
30
 
20
31
  async init(app: App) {
32
+ this.app = app;
21
33
  // If we want to replace the main 'db' object or sit alongside it
22
34
  app.context.set('oracle', this);
23
35
  }
24
36
 
25
37
  async start(app?: App) { // app optional to satisfy interface but we might need config from it
38
+ if (app) this.app = app;
39
+
26
40
  // Check env vars
27
41
  if (!process.env.ORA_CONN) {
28
- console.warn('Oracle connection string (ORA_CONN) not set. Oracle driver will not start.');
42
+ this.log('warn', 'Oracle connection string (ORA_CONN) not set. Oracle driver will not start.');
29
43
  return;
30
44
  }
31
45
 
@@ -52,6 +66,33 @@ export class OracleDriver implements Driver {
52
66
  }
53
67
  }
54
68
 
69
+ private log(level: 'error' | 'warn', objOrMsg: unknown, msg?: string) {
70
+ // Route all diagnostics through app.logger; never use the global console.
71
+ if (!this.app) return;
72
+ const logger = this.app.logger;
73
+ if (typeof objOrMsg === 'string') {
74
+ logger[level](objOrMsg);
75
+ } else {
76
+ logger[level](objOrMsg as object, msg);
77
+ }
78
+ }
79
+
80
+ private settle(id: number, action: (entry: PendingEntry) => void) {
81
+ const entry = this.pending.get(id);
82
+ if (!entry) return;
83
+ if (entry.timer) clearTimeout(entry.timer);
84
+ this.pending.delete(id);
85
+ action(entry);
86
+ }
87
+
88
+ private rejectAllPending(err: Error) {
89
+ if (this.pending.size === 0) return;
90
+ const ids = Array.from(this.pending.keys());
91
+ for (const id of ids) {
92
+ this.settle(id, ({ reject }) => reject(err));
93
+ }
94
+ }
95
+
55
96
  async query(sql: string, params: any[] = []) {
56
97
  if (!this.proc || !this.proc.stdin) {
57
98
  throw new Error('Oracle driver not started');
@@ -60,13 +101,24 @@ export class OracleDriver implements Driver {
60
101
  const id = this.reqId++;
61
102
 
62
103
  return new Promise((resolve, reject) => {
63
- this.pending.set(id, { resolve, reject });
104
+ const timer = setTimeout(() => {
105
+ this.settle(id, ({ reject: rej }) =>
106
+ rej(new Error(`Oracle query timed out after ${this.timeoutMs}ms`)),
107
+ );
108
+ }, this.timeoutMs);
109
+
110
+ this.pending.set(id, { resolve, reject, timer });
64
111
 
65
112
  const msg = JSON.stringify({ id, sql, params }) + '\n';
66
- const stdin = this.proc!.stdin as any; // Bun FileSink/writer
67
- if (stdin.write) {
113
+ const stdin = this.proc!.stdin as FileSink;
114
+ if (typeof stdin.write === 'function') {
68
115
  stdin.write(msg);
69
116
  stdin.flush();
117
+ } else {
118
+ // No writable stdin: do not leave the request hanging in pending.
119
+ this.settle(id, ({ reject: rej }) =>
120
+ rej(new Error('Oracle bridge stdin is not writable')),
121
+ );
70
122
  }
71
123
  });
72
124
  }
@@ -86,39 +138,43 @@ export class OracleDriver implements Driver {
86
138
  buffer = lines.pop() || ''; // Keep incomplete line
87
139
 
88
140
  for (const line of lines) {
89
- if (!line.trim()) continue;
90
- try {
91
- const msg = JSON.parse(line);
92
-
93
- if (msg.type === 'ready') {
94
- // console.log('Oracle Bridge Ready');
95
- continue;
96
- }
97
- if (msg.type === 'fatal') {
98
- console.error('Oracle Bridge Fatal Error:', msg.error);
99
- // Reject all pending?
100
- continue;
101
- }
102
-
103
- if (msg.id !== undefined && this.pending.has(msg.id)) {
104
- const { resolve, reject } = this.pending.get(msg.id)!;
105
- this.pending.delete(msg.id);
106
-
107
- if (msg.error) {
108
- reject(new Error(msg.error));
109
- } else {
110
- resolve(msg.data);
111
- }
112
- }
113
- } catch (err) {
114
- console.error('Error parsing bridge message:', err, line);
115
- }
141
+ this.handleLine(line);
116
142
  }
117
143
  }
118
144
  } catch (err) {
119
- console.error('Error reading from Oracle bridge:', err);
145
+ this.log('error', { err }, 'Error reading from Oracle bridge');
146
+ this.rejectAllPending(new Error('Oracle bridge stream error'));
120
147
  } finally {
121
148
  reader.releaseLock();
149
+ this.rejectAllPending(new Error('Oracle bridge process exited'));
150
+ }
151
+ }
152
+
153
+ private handleLine(line: string) {
154
+ if (!line.trim()) return;
155
+ try {
156
+ const msg = JSON.parse(line);
157
+
158
+ if (msg.type === 'ready') {
159
+ return;
160
+ }
161
+ if (msg.type === 'fatal') {
162
+ this.log('error', { error: msg.error }, 'Oracle Bridge Fatal Error');
163
+ this.rejectAllPending(new Error(`Oracle bridge fatal: ${msg.error}`));
164
+ return;
165
+ }
166
+
167
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
168
+ this.settle(msg.id, ({ resolve, reject }) => {
169
+ if (msg.error) {
170
+ reject(new Error(msg.error));
171
+ } else {
172
+ resolve(msg.data);
173
+ }
174
+ });
175
+ }
176
+ } catch (err) {
177
+ this.log('error', { err, line }, 'Error parsing bridge message');
122
178
  }
123
179
  }
124
180
  }