@docverse-pdf/server 1.1.0 → 1.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.
@@ -1,6 +1,12 @@
1
1
  export interface UnoServerOptions {
2
- /** TCP port the unoserver daemon listens on (default: 2003). */
2
+ /** TCP port the unoserver daemon listens on for its own RPC (default: 2003). */
3
3
  port?: number;
4
+ /**
5
+ * TCP port used by the underlying LibreOffice UNO socket. Must be unique per
6
+ * daemon — otherwise multiple daemons collide on the default 2002.
7
+ * (default: undefined = unoserver picks 2002, which only works for one daemon.)
8
+ */
9
+ unoPort?: number;
4
10
  /** Host/interface the daemon binds to (default: 127.0.0.1). */
5
11
  host?: string;
6
12
  /** Path to the `unoserver` executable (default: auto-detect). */
@@ -9,18 +15,27 @@ export interface UnoServerOptions {
9
15
  unoconvertPath?: string;
10
16
  /** Path to the LibreOffice `soffice` binary (passed to unoserver --executable). */
11
17
  libreOfficePath?: string;
12
- /** Dedicated UNO user-profile dir for this daemon (passed to unoserver --user-installation). */
18
+ /**
19
+ * Dedicated UNO user-profile dir for this daemon (passed to unoserver
20
+ * --user-installation). **Must be an absolute filesystem path, not a
21
+ * `file://` URI** — unoserver converts it internally via `Path(...).as_uri()`.
22
+ */
13
23
  userInstallation?: string;
14
24
  /** Max time (ms) to wait for the daemon to become ready (default: 15000). */
15
25
  startTimeout?: number;
16
26
  /** Max time (ms) to wait for a single conversion (default: 30000). */
17
27
  convertTimeout?: number;
18
28
  }
19
- export interface UnoServerPoolOptions extends Omit<UnoServerOptions, 'port' | 'userInstallation'> {
29
+ export interface UnoServerPoolOptions extends Omit<UnoServerOptions, 'port' | 'unoPort' | 'userInstallation'> {
20
30
  /** Number of parallel unoserver daemons (default: 2). */
21
31
  workers?: number;
22
- /** First TCP port; each worker gets basePort+i (default: 2003). */
32
+ /** First daemon RPC port; each worker gets basePort+i (default: 2003). */
23
33
  basePort?: number;
34
+ /**
35
+ * First LibreOffice UNO-socket port; each worker gets unoBasePort+i
36
+ * (default: 2100). Must be in a non-overlapping range with `basePort`.
37
+ */
38
+ unoBasePort?: number;
24
39
  /** Base dir for per-worker user-profile dirs (default: os.tmpdir()/docverse_unoserver). */
25
40
  tempDir?: string;
26
41
  }
package/dist/unoServer.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawn, execSync } from 'child_process';
2
2
  import { createConnection } from 'net';
3
+ import { resolve as resolvePath } from 'path';
3
4
  // ═══════════════════════════════════════════════
4
5
  // HELPERS
5
6
  // ═══════════════════════════════════════════════
@@ -104,11 +105,14 @@ export class UnoServer {
104
105
  constructor(options = {}) {
105
106
  this.opts = {
106
107
  port: options.port ?? 2003,
108
+ unoPort: options.unoPort,
107
109
  host: options.host ?? '127.0.0.1',
108
110
  unoserverPath: options.unoserverPath ?? findUnoserver(),
109
111
  unoconvertPath: options.unoconvertPath ?? findUnoconvert(),
110
112
  libreOfficePath: options.libreOfficePath ?? findSoffice(),
111
- userInstallation: options.userInstallation,
113
+ userInstallation: options.userInstallation
114
+ ? resolvePath(options.userInstallation)
115
+ : undefined,
112
116
  startTimeout: options.startTimeout ?? 15000,
113
117
  convertTimeout: options.convertTimeout ?? 30000,
114
118
  };
@@ -126,6 +130,9 @@ export class UnoServer {
126
130
  if (this.ready)
127
131
  return;
128
132
  const args = ['--interface', this.opts.host, '--port', String(this.opts.port)];
133
+ if (this.opts.unoPort !== undefined) {
134
+ args.push('--uno-port', String(this.opts.unoPort));
135
+ }
129
136
  if (this.opts.libreOfficePath) {
130
137
  args.push('--executable', this.opts.libreOfficePath);
131
138
  }
@@ -136,24 +143,52 @@ export class UnoServer {
136
143
  stdio: ['ignore', 'pipe', 'pipe'],
137
144
  detached: false,
138
145
  });
139
- proc.on('error', (err) => {
140
- this.ready = false;
141
- // Let the caller see the launch error via waitForPort timeout.
142
- // Emit to stderr so it's debuggable without swallowing silently.
143
- process.stderr.write(`[unoserver] spawn error: ${err.message}\n`);
146
+ this.proc = proc;
147
+ // Capture stderr so startup failures surface the real reason, not just a
148
+ // generic "did not become ready" timeout.
149
+ const stderrChunks = [];
150
+ proc.stderr?.on('data', (c) => stderrChunks.push(c));
151
+ // Also drain stdout (unoserver is quiet here but we must consume it to
152
+ // avoid filling the pipe buffer on some platforms).
153
+ proc.stdout?.on('data', () => { });
154
+ const collectStderr = () => Buffer.concat(stderrChunks).toString('utf8').trim();
155
+ let earlyExit = null;
156
+ const earlyExitPromise = new Promise((_, reject) => {
157
+ proc.once('exit', (code, signal) => {
158
+ this.ready = false;
159
+ this.proc = null;
160
+ if (earlyExit === null) {
161
+ earlyExit = { code, signal };
162
+ reject(new Error(`unoserver exited during startup (code=${code}, signal=${signal})\n` +
163
+ `stderr:\n${collectStderr() || '(empty)'}`));
164
+ }
165
+ });
144
166
  });
145
- proc.on('exit', () => {
146
- this.ready = false;
147
- this.proc = null;
167
+ const spawnErrorPromise = new Promise((_, reject) => {
168
+ proc.once('error', (err) => {
169
+ this.ready = false;
170
+ reject(new Error(`unoserver spawn error: ${err.message}`));
171
+ });
148
172
  });
149
- this.proc = proc;
150
173
  const deadline = Date.now() + this.opts.startTimeout;
151
174
  try {
152
- await waitForPort(this.opts.host, this.opts.port, deadline);
175
+ await Promise.race([
176
+ waitForPort(this.opts.host, this.opts.port, deadline),
177
+ earlyExitPromise,
178
+ spawnErrorPromise,
179
+ ]);
153
180
  }
154
181
  catch (err) {
155
182
  await this.stop();
156
- throw err;
183
+ if (earlyExit !== null) {
184
+ // Early-exit error is already descriptive; rethrow as-is.
185
+ throw err;
186
+ }
187
+ // Timeout path — attach whatever stderr we collected for debugging.
188
+ const stderr = collectStderr();
189
+ const message = `unoserver did not become ready on ${this.opts.host}:${this.opts.port} ` +
190
+ `within ${this.opts.startTimeout}ms\nstderr:\n${stderr || '(empty)'}`;
191
+ throw new Error(message);
157
192
  }
158
193
  this.ready = true;
159
194
  }
@@ -257,6 +292,7 @@ export class UnoServerPool {
257
292
  this.opts = {
258
293
  workers: options.workers ?? 2,
259
294
  basePort: options.basePort ?? 2003,
295
+ unoBasePort: options.unoBasePort ?? 2100,
260
296
  tempDir: options.tempDir ?? `${process.env.TMPDIR ?? '/tmp'}/docverse_unoserver`,
261
297
  startTimeout: options.startTimeout ?? 15000,
262
298
  convertTimeout: options.convertTimeout ?? 30000,
@@ -275,9 +311,12 @@ export class UnoServerPool {
275
311
  }
276
312
  const servers = [];
277
313
  for (let i = 0; i < this.opts.workers; i++) {
278
- const userInstallation = `file://${this.opts.tempDir}/worker_${i}`;
314
+ // unoserver expects a raw absolute path for --user-installation; passing
315
+ // a `file://` URI trips its internal `Path(...).as_uri()` and crashes.
316
+ const userInstallation = resolvePath(this.opts.tempDir, `worker_${i}`);
279
317
  servers.push(new UnoServer({
280
318
  port: this.opts.basePort + i,
319
+ unoPort: this.opts.unoBasePort + i,
281
320
  host: this.opts.host,
282
321
  unoserverPath: this.opts.unoserverPath,
283
322
  unoconvertPath: this.opts.unoconvertPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docverse-pdf/server",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "DocVerse Server SDK — server-side PDF processing, XFDF merge, Word to PDF (unoserver + LibreOffice), image extraction",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",