@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 +10 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +73 -27
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/driver.ts +91 -35
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|