postgres-memory-server 0.2.0 → 0.2.2

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/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/PostgresMemoryServer.ts
2
- import { promises as fs2 } from "fs";
2
+ import { promises as fs2, rmSync } from "fs";
3
3
  import os2 from "os";
4
4
  import path2 from "path";
5
+ import process2 from "process";
5
6
  import EmbeddedPostgres from "embedded-postgres";
6
7
  import { Client } from "pg";
7
8
 
@@ -62,7 +63,8 @@ function getPgMajorVersion() {
62
63
  const content = readFileSync(candidate, "utf8");
63
64
  const pkg = JSON.parse(content);
64
65
  if (pkg.name === "embedded-postgres" && pkg.version) {
65
- return pkg.version.split(".")[0];
66
+ const major = pkg.version.split(".")[0];
67
+ if (major) return major;
66
68
  }
67
69
  } catch {
68
70
  }
@@ -208,7 +210,7 @@ function buildDownloadUrl(version, pgMajorVersion, platform, arch) {
208
210
  }
209
211
  function getMacOSCodename() {
210
212
  const release = os.release();
211
- const majorVersion = parseInt(release.split(".")[0], 10);
213
+ const majorVersion = parseInt(release.split(".")[0] ?? "0", 10);
212
214
  if (majorVersion >= 24) return "sequoia";
213
215
  if (majorVersion >= 23) return "sonoma";
214
216
  throw new Error(
@@ -373,7 +375,7 @@ async function installPgVectorExtension(nativeDir, pgMajorVersion) {
373
375
  function getHomebrewBottleTag(platform, arch) {
374
376
  if (platform === "darwin") {
375
377
  const release = os.release();
376
- const major = parseInt(release.split(".")[0], 10);
378
+ const major = parseInt(release.split(".")[0] ?? "0", 10);
377
379
  const prefix = arch === "arm64" ? "arm64_" : "";
378
380
  if (major >= 25) return `${prefix}tahoe`;
379
381
  if (major >= 24) return `${prefix}sequoia`;
@@ -385,9 +387,61 @@ function getHomebrewBottleTag(platform, arch) {
385
387
  }
386
388
  throw new Error(`No Homebrew bottles available for ${platform}-${arch}`);
387
389
  }
390
+ var ORPHAN_MIN_AGE_MS = 6e4;
391
+ async function sweepOrphanedDataDirs(minAgeMs = ORPHAN_MIN_AGE_MS) {
392
+ const tmpDir = os.tmpdir();
393
+ let entries;
394
+ try {
395
+ entries = await fs.readdir(tmpDir);
396
+ } catch {
397
+ return;
398
+ }
399
+ const cutoff = Date.now() - minAgeMs;
400
+ await Promise.all(
401
+ entries.filter((name) => name.startsWith("postgres-memory-server-")).map(async (name) => {
402
+ const fullPath = path.join(tmpDir, name);
403
+ let stat;
404
+ try {
405
+ stat = await fs.stat(fullPath);
406
+ if (!stat.isDirectory()) return;
407
+ } catch {
408
+ return;
409
+ }
410
+ const pidFile = path.join(fullPath, "postmaster.pid");
411
+ let pid = null;
412
+ let pidFileExists = false;
413
+ try {
414
+ const content = await fs.readFile(pidFile, "utf8");
415
+ pidFileExists = true;
416
+ const firstLine = content.split("\n")[0]?.trim();
417
+ const parsed = firstLine ? parseInt(firstLine, 10) : NaN;
418
+ if (!Number.isNaN(parsed) && parsed > 0) {
419
+ pid = parsed;
420
+ }
421
+ } catch {
422
+ }
423
+ if (pid !== null) {
424
+ try {
425
+ process.kill(pid, 0);
426
+ return;
427
+ } catch (err) {
428
+ const code = err.code;
429
+ if (code === "EPERM") {
430
+ return;
431
+ }
432
+ }
433
+ }
434
+ if (!pidFileExists && stat.mtimeMs > cutoff) {
435
+ return;
436
+ }
437
+ await fs.rm(fullPath, { recursive: true, force: true }).catch(() => {
438
+ });
439
+ })
440
+ );
441
+ }
388
442
  function parseParadeDBVersion(version) {
389
443
  const match = version.match(/^(\d+\.\d+\.\d+)(?:-pg(\d+))?$/);
390
- if (!match) {
444
+ if (!match || !match[1]) {
391
445
  return { extVersion: version };
392
446
  }
393
447
  return {
@@ -473,6 +527,30 @@ function quoteIdentifier(name) {
473
527
  }
474
528
 
475
529
  // src/PostgresMemoryServer.ts
530
+ var liveInstances = /* @__PURE__ */ new Set();
531
+ var exitHandlersRegistered = false;
532
+ var orphanSweepDone = false;
533
+ function registerExitHandlers() {
534
+ if (exitHandlersRegistered) return;
535
+ exitHandlersRegistered = true;
536
+ const cleanup = () => {
537
+ for (const instance of liveInstances) {
538
+ try {
539
+ instance._cleanupSync();
540
+ } catch {
541
+ }
542
+ }
543
+ };
544
+ process2.once("exit", cleanup);
545
+ const signalCleanup = (signal) => {
546
+ cleanup();
547
+ process2.removeListener(signal, signalCleanup);
548
+ process2.kill(process2.pid, signal);
549
+ };
550
+ process2.on("SIGINT", signalCleanup);
551
+ process2.on("SIGTERM", signalCleanup);
552
+ process2.on("SIGHUP", signalCleanup);
553
+ }
476
554
  var PostgresMemoryServer = class _PostgresMemoryServer {
477
555
  constructor(pg, port, dataDir, options) {
478
556
  this.pg = pg;
@@ -485,64 +563,81 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
485
563
  snapshotSupported;
486
564
  hasSnapshot = false;
487
565
  static async create(options = {}) {
566
+ if (!orphanSweepDone) {
567
+ orphanSweepDone = true;
568
+ await sweepOrphanedDataDirs().catch(() => {
569
+ });
570
+ }
488
571
  const normalized = normalizeOptions(options);
489
572
  const port = await getFreePort();
490
573
  const dataDir = await fs2.mkdtemp(
491
574
  path2.join(os2.tmpdir(), "postgres-memory-server-")
492
575
  );
493
- const postgresFlags = [];
494
- if (normalized.preset === "paradedb") {
495
- const nativeDir = getNativeDir();
496
- const extVersion = resolveParadeDBVersion(normalized.version);
497
- const pgMajor = DEFAULT_POSTGRES_VERSION;
498
- try {
499
- await installParadeDBExtension(nativeDir, extVersion, pgMajor);
500
- } catch (error) {
501
- throw new ExtensionInstallError(
502
- "pg_search",
503
- error instanceof Error ? error : new Error(String(error))
504
- );
505
- }
506
- if (normalized.extensions.includes("vector")) {
576
+ let pg;
577
+ try {
578
+ const postgresFlags = [];
579
+ if (normalized.preset === "paradedb") {
580
+ const nativeDir = getNativeDir();
581
+ const extVersion = resolveParadeDBVersion(normalized.version);
582
+ const pgMajor = DEFAULT_POSTGRES_VERSION;
507
583
  try {
508
- await installPgVectorExtension(nativeDir, pgMajor);
584
+ await installParadeDBExtension(nativeDir, extVersion, pgMajor);
509
585
  } catch (error) {
510
586
  throw new ExtensionInstallError(
511
- "vector",
587
+ "pg_search",
512
588
  error instanceof Error ? error : new Error(String(error))
513
589
  );
514
590
  }
591
+ if (normalized.extensions.includes("vector")) {
592
+ try {
593
+ await installPgVectorExtension(nativeDir, pgMajor);
594
+ } catch (error) {
595
+ throw new ExtensionInstallError(
596
+ "vector",
597
+ error instanceof Error ? error : new Error(String(error))
598
+ );
599
+ }
600
+ }
601
+ if (normalized.extensions.includes("pg_search") || normalized.extensions.length === 0) {
602
+ postgresFlags.push("-c", "shared_preload_libraries=pg_search");
603
+ }
515
604
  }
516
- if (normalized.extensions.includes("pg_search") || normalized.extensions.length === 0) {
517
- postgresFlags.push(
518
- "-c",
519
- "shared_preload_libraries=pg_search"
520
- );
605
+ pg = new EmbeddedPostgres({
606
+ databaseDir: dataDir,
607
+ port,
608
+ user: normalized.username,
609
+ password: normalized.password,
610
+ persistent: false,
611
+ postgresFlags,
612
+ onLog: () => {
613
+ },
614
+ onError: () => {
615
+ }
616
+ });
617
+ await pg.initialise();
618
+ await pg.start();
619
+ if (normalized.database !== "postgres") {
620
+ await pg.createDatabase(normalized.database);
521
621
  }
522
- }
523
- const pg = new EmbeddedPostgres({
524
- databaseDir: dataDir,
525
- port,
526
- user: normalized.username,
527
- password: normalized.password,
528
- persistent: false,
529
- postgresFlags,
530
- onLog: () => {
531
- },
532
- onError: () => {
622
+ const server = new _PostgresMemoryServer(pg, port, dataDir, normalized);
623
+ liveInstances.add(server);
624
+ registerExitHandlers();
625
+ const initStatements = buildInitStatements(normalized);
626
+ if (initStatements.length > 0) {
627
+ await server.runSql(initStatements);
533
628
  }
534
- });
535
- await pg.initialise();
536
- await pg.start();
537
- if (normalized.database !== "postgres") {
538
- await pg.createDatabase(normalized.database);
539
- }
540
- const server = new _PostgresMemoryServer(pg, port, dataDir, normalized);
541
- const initStatements = buildInitStatements(normalized);
542
- if (initStatements.length > 0) {
543
- await server.runSql(initStatements);
629
+ return server;
630
+ } catch (error) {
631
+ if (pg) {
632
+ try {
633
+ await pg.stop();
634
+ } catch {
635
+ }
636
+ }
637
+ await fs2.rm(dataDir, { recursive: true, force: true }).catch(() => {
638
+ });
639
+ throw error;
544
640
  }
545
- return server;
546
641
  }
547
642
  static createPostgres(options = {}) {
548
643
  return _PostgresMemoryServer.create({ ...options, preset: "postgres" });
@@ -574,6 +669,14 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
574
669
  getImage() {
575
670
  return this.options.image;
576
671
  }
672
+ /**
673
+ * Returns the absolute path to the temporary PostgreSQL data directory
674
+ * for this instance. Useful for debugging or backing up state. The
675
+ * directory is automatically removed by `stop()`.
676
+ */
677
+ getDataDir() {
678
+ return this.dataDir;
679
+ }
577
680
  getConnectionOptions() {
578
681
  return {
579
682
  host: this.getHost(),
@@ -679,7 +782,32 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
679
782
  return;
680
783
  }
681
784
  this.stopped = true;
682
- await this.pg.stop();
785
+ liveInstances.delete(this);
786
+ try {
787
+ await this.pg.stop();
788
+ } catch {
789
+ }
790
+ await fs2.rm(this.dataDir, { recursive: true, force: true }).catch(() => {
791
+ });
792
+ }
793
+ /**
794
+ * Synchronous cleanup for use in process exit handlers. Cannot await,
795
+ * so we just remove the data directory and let the OS reap the postgres
796
+ * child process. embedded-postgres registers its own exit hook to kill
797
+ * the process; this method is a backup for the data directory only.
798
+ *
799
+ * @internal
800
+ */
801
+ _cleanupSync() {
802
+ if (this.stopped) {
803
+ return;
804
+ }
805
+ this.stopped = true;
806
+ liveInstances.delete(this);
807
+ try {
808
+ rmSync(this.dataDir, { recursive: true, force: true });
809
+ } catch {
810
+ }
683
811
  }
684
812
  /**
685
813
  * Connect to the "postgres" system database for admin operations
@@ -718,7 +846,7 @@ import { spawn } from "child_process";
718
846
  import { createHash } from "crypto";
719
847
  import path3 from "path";
720
848
  import { tmpdir } from "os";
721
- import process2 from "process";
849
+ import process3 from "process";
722
850
  import { fileURLToPath, pathToFileURL } from "url";
723
851
  var CHILD_OPTIONS_ENV_VAR = "POSTGRES_MEMORY_SERVER_CHILD_OPTIONS_B64";
724
852
  var CHILD_SETUP_TIMEOUT_MS = 12e4;
@@ -727,7 +855,7 @@ var POLL_INTERVAL_MS = 100;
727
855
  var DEFAULT_JEST_ENV_VAR_NAME = "DATABASE_URL";
728
856
  var DEFAULT_JEST_STATE_FILE = path3.join(
729
857
  tmpdir(),
730
- `postgres-memory-server-jest-${createHash("sha256").update(process2.cwd()).digest("hex").slice(0, 12)}.json`
858
+ `postgres-memory-server-jest-${createHash("sha256").update(process3.cwd()).digest("hex").slice(0, 12)}.json`
731
859
  );
732
860
  function getChildScript(childModuleUrl) {
733
861
  return `
@@ -823,24 +951,24 @@ async function readStateFile(filePath) {
823
951
  }
824
952
  }
825
953
  function applyConnectionEnvironment(envVarName, payload) {
826
- process2.env[envVarName] = payload.uri;
827
- process2.env.POSTGRES_MEMORY_SERVER_URI = payload.uri;
828
- process2.env.POSTGRES_MEMORY_SERVER_HOST = payload.host;
829
- process2.env.POSTGRES_MEMORY_SERVER_PORT = String(payload.port);
830
- process2.env.POSTGRES_MEMORY_SERVER_DATABASE = payload.database;
831
- process2.env.POSTGRES_MEMORY_SERVER_USERNAME = payload.username;
832
- process2.env.POSTGRES_MEMORY_SERVER_PASSWORD = payload.password;
833
- process2.env.POSTGRES_MEMORY_SERVER_IMAGE = payload.image;
954
+ process3.env[envVarName] = payload.uri;
955
+ process3.env.POSTGRES_MEMORY_SERVER_URI = payload.uri;
956
+ process3.env.POSTGRES_MEMORY_SERVER_HOST = payload.host;
957
+ process3.env.POSTGRES_MEMORY_SERVER_PORT = String(payload.port);
958
+ process3.env.POSTGRES_MEMORY_SERVER_DATABASE = payload.database;
959
+ process3.env.POSTGRES_MEMORY_SERVER_USERNAME = payload.username;
960
+ process3.env.POSTGRES_MEMORY_SERVER_PASSWORD = payload.password;
961
+ process3.env.POSTGRES_MEMORY_SERVER_IMAGE = payload.image;
834
962
  }
835
963
  async function startChildProcess(options) {
836
964
  const childModuleUrl = await resolveChildModuleUrl();
837
965
  return new Promise((resolve, reject) => {
838
966
  const child = spawn(
839
- process2.execPath,
967
+ process3.execPath,
840
968
  ["--input-type=module", "--eval", getChildScript(childModuleUrl)],
841
969
  {
842
970
  env: {
843
- ...process2.env,
971
+ ...process3.env,
844
972
  [CHILD_OPTIONS_ENV_VAR]: Buffer.from(
845
973
  JSON.stringify(options),
846
974
  "utf8"
@@ -936,7 +1064,7 @@ async function resolveChildModuleUrl() {
936
1064
  }
937
1065
  async function stopChildProcess(pid) {
938
1066
  try {
939
- process2.kill(pid, "SIGTERM");
1067
+ process3.kill(pid, "SIGTERM");
940
1068
  } catch (error) {
941
1069
  if (isMissingProcessError(error)) {
942
1070
  return;
@@ -947,7 +1075,7 @@ async function stopChildProcess(pid) {
947
1075
  while (Date.now() < deadline) {
948
1076
  await sleep(POLL_INTERVAL_MS);
949
1077
  try {
950
- process2.kill(pid, 0);
1078
+ process3.kill(pid, 0);
951
1079
  } catch (error) {
952
1080
  if (isMissingProcessError(error)) {
953
1081
  return;