diff --git a/changelog.md b/changelog.md index 6c0f44a..1a7f29b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2026-02-03 - 5.0.0 - BREAKING CHANGE(localtsmdb) +add Unix socket support and change LocalTsmDb API to return connection info instead of a MongoClient + +- LocalTsmDb.start() now returns ILocalTsmDbConnectionInfo { socketPath, connectionUri } instead of a connected MongoClient +- Removed internal MongoClient management: consumers must create/connect/close their own MongoClient using the returned connectionUri (close client before calling db.stop()) +- Added ILocalTsmDbConnectionInfo type and getConnectionInfo() (replaces getClient()) +- TsmdbServer: added socketPath option to listen on Unix sockets, cleans up stale socket files on start/stop, and encodes socket paths in getConnectionUri() +- LocalTsmDb can auto-generate socket paths in the OS temp dir; LocalTsmDb no longer depends on the mongodb package internally (lightweight Unix socket wrapper) +- Updated docs and tests to use MongoClient externally and to demonstrate socketPath/connectionUri workflow +- ts_local plugins no longer export net (net usage moved to server implementation) + ## 2026-02-03 - 4.3.0 - feat(docs) add LocalTsmDb documentation and examples; update README code samples and imports; correct examples and variable names; update package author diff --git a/readme.md b/readme.md index b8e6968..1f42383 100644 --- a/readme.md +++ b/readme.md @@ -21,9 +21,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community | Feature | SmartMongo | TsmDB | LocalTsmDb | |---------|------------|-------|------------| | **Type** | Real MongoDB (memory server) | Wire protocol server | Zero-config local DB | -| **Speed** | ~2-5s startup | ⚡ Instant (~5ms) | ⚡ Instant + auto-connect | +| **Speed** | ~2-5s startup | ⚡ Instant (~5ms) | ⚡ Instant (Unix socket) | | **Compatibility** | 100% MongoDB | MongoDB driver compatible | MongoDB driver compatible | -| **Dependencies** | Downloads MongoDB binary | Zero external deps | Zero external deps | +| **Dependencies** | Downloads MongoDB binary | Zero external deps | Zero external deps (no MongoDB driver!) | | **Replication** | ✅ Full replica set | Single node | Single node | | **Persistence** | Dump to directory | Memory or file | File-based (automatic) | | **Use Case** | Integration testing | Unit testing, CI/CD | Quick prototyping, local dev | @@ -32,16 +32,21 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community ### Option 1: LocalTsmDb (Zero-Config Local Database) ⭐ NEW -The easiest way to get started — just point it at a folder and you have a persistent MongoDB-compatible database with automatic port discovery! +The easiest way to get started — just point it at a folder and you have a persistent MongoDB-compatible database using Unix sockets. No port conflicts, no MongoDB driver dependency in LocalTsmDb! ```typescript import { LocalTsmDb } from '@push.rocks/smartmongo'; +import { MongoClient } from 'mongodb'; // Create a local database backed by files const db = new LocalTsmDb({ folderPath: './my-data' }); -// Start and get a connected MongoDB client -const client = await db.start(); +// Start and get connection info (Unix socket path + connection URI) +const { connectionUri } = await db.start(); + +// Connect with your own MongoDB client +const client = new MongoClient(connectionUri, { directConnection: true }); +await client.connect(); // Use exactly like MongoDB const users = client.db('myapp').collection('users'); @@ -51,11 +56,14 @@ const user = await users.findOne({ name: 'Alice' }); console.log(user); // { _id: ObjectId(...), name: 'Alice', email: 'alice@example.com' } // Data persists to disk automatically! +await client.close(); await db.stop(); // Later... data is still there const db2 = new LocalTsmDb({ folderPath: './my-data' }); -const client2 = await db2.start(); +const { connectionUri: uri2 } = await db2.start(); +const client2 = new MongoClient(uri2, { directConnection: true }); +await client2.connect(); const savedUser = await client2.db('myapp').collection('users').findOne({ name: 'Alice' }); // savedUser exists! ``` @@ -111,21 +119,31 @@ await mongo.stop(); ## 📖 LocalTsmDb API -The simplest option for local development and prototyping — zero config, auto port discovery, and automatic persistence. +The simplest option for local development and prototyping — lightweight, Unix socket-based, and automatic persistence. ### Basic Usage ```typescript import { LocalTsmDb } from '@push.rocks/smartmongo'; +import { MongoClient } from 'mongodb'; const db = new LocalTsmDb({ folderPath: './data', // Required: where to store data - port: 27017, // Optional: defaults to auto-discovery - host: '127.0.0.1', // Optional: bind address + socketPath: '/tmp/my.sock', // Optional: custom socket path (default: auto-generated) }); -// Start and get connected client -const client = await db.start(); +// Start and get connection info +const { socketPath, connectionUri } = await db.start(); +console.log(socketPath); // /tmp/smartmongo-abc123.sock (auto-generated) +console.log(connectionUri); // mongodb://%2Ftmp%2Fsmartmongo-abc123.sock + +// Connect with your own MongoDB client +const client = new MongoClient(connectionUri, { directConnection: true }); +await client.connect(); + +// Use the client +const users = client.db('mydb').collection('users'); +await users.insertOne({ name: 'Alice' }); // Access the underlying server if needed const server = db.getServer(); @@ -134,16 +152,18 @@ const uri = db.getConnectionUri(); // Check status console.log(db.running); // true -// Stop when done +// Stop when done (close your client first!) +await client.close(); await db.stop(); ``` ### Features -- 🔍 **Auto Port Discovery** — Automatically finds an available port if 27017 is in use +- 🔌 **Unix Sockets** — No port conflicts, faster IPC than TCP - 💾 **Automatic Persistence** — Data saved to files, survives restarts -- 🔌 **Pre-connected Client** — `start()` returns a ready-to-use MongoDB client +- 🪶 **Lightweight** — No MongoDB driver dependency in LocalTsmDb itself - 🎯 **Zero Config** — Just specify a folder path and you're good to go +- 🔗 **Connection URI** — Ready-to-use URI for your own MongoClient ## 📖 SmartMongo API @@ -492,10 +512,13 @@ let client: MongoClient; beforeAll(async () => { db = new LocalTsmDb({ folderPath: './test-data' }); - client = await db.start(); + const { connectionUri } = await db.start(); + client = new MongoClient(connectionUri, { directConnection: true }); + await client.connect(); }); afterAll(async () => { + await client.close(); await db.stop(); }); @@ -555,16 +578,19 @@ test('should insert and find user', async () => { ```typescript import { expect, tap } from '@git.zone/tstest/tapbundle'; import { LocalTsmDb } from '@push.rocks/smartmongo'; +import { MongoClient } from 'mongodb'; let db: LocalTsmDb; +let client: MongoClient; tap.test('setup', async () => { db = new LocalTsmDb({ folderPath: './test-data' }); - await db.start(); + const { connectionUri } = await db.start(); + client = new MongoClient(connectionUri, { directConnection: true }); + await client.connect(); }); tap.test('should perform CRUD operations', async () => { - const client = db.getClient(); const col = client.db('test').collection('items'); // Create @@ -587,6 +613,7 @@ tap.test('should perform CRUD operations', async () => { }); tap.test('teardown', async () => { + await client.close(); await db.stop(); }); @@ -601,7 +628,7 @@ export default tap.start(); @push.rocks/smartmongo ├── SmartMongo → Real MongoDB memory server (mongodb-memory-server wrapper) ├── tsmdb → Wire protocol server with full engine stack -└── LocalTsmDb → Zero-config local database (convenience wrapper) +└── LocalTsmDb → Lightweight Unix socket wrapper (no MongoDB driver dependency) ``` ### TsmDB Wire Protocol Stack diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index e75ddde..6bb66a3 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartmongo', - version: '4.3.0', + version: '5.0.0', description: 'A module for creating and managing a local MongoDB instance for testing purposes.' } diff --git a/ts/ts_local/classes.localtsmdb.ts b/ts/ts_local/classes.localtsmdb.ts index 5b4ad26..eeed99a 100644 --- a/ts/ts_local/classes.localtsmdb.ts +++ b/ts/ts_local/classes.localtsmdb.ts @@ -1,98 +1,106 @@ import * as plugins from './plugins.js'; +import * as crypto from 'crypto'; +import * as path from 'path'; +import * as os from 'os'; import { TsmdbServer } from '../ts_tsmdb/index.js'; -import type { MongoClient } from 'mongodb'; + +/** + * Connection information returned by LocalTsmDb.start() + */ +export interface ILocalTsmDbConnectionInfo { + /** The Unix socket file path */ + socketPath: string; + /** MongoDB connection URI ready for MongoClient */ + connectionUri: string; +} export interface ILocalTsmDbOptions { + /** Required: where to store data */ folderPath: string; - port?: number; - host?: string; + /** Optional: custom socket path (default: auto-generated in /tmp) */ + socketPath?: string; } /** - * LocalTsmDb - Convenience class for local MongoDB-compatible database + * LocalTsmDb - Lightweight local MongoDB-compatible database using Unix sockets * * This class wraps TsmdbServer and provides a simple interface for - * starting a local file-based MongoDB-compatible server and connecting to it. + * starting a local file-based MongoDB-compatible server. Returns connection + * info that you can use with your own MongoDB driver instance. * * @example * ```typescript * import { LocalTsmDb } from '@push.rocks/smartmongo'; + * import { MongoClient } from 'mongodb'; * * const db = new LocalTsmDb({ folderPath: './data' }); - * const client = await db.start(); + * const { connectionUri } = await db.start(); + * + * // Connect with your own MongoDB client + * const client = new MongoClient(connectionUri, { directConnection: true }); + * await client.connect(); * * // Use the MongoDB client * const collection = client.db('mydb').collection('users'); * await collection.insertOne({ name: 'Alice' }); * * // When done + * await client.close(); * await db.stop(); * ``` */ export class LocalTsmDb { private options: ILocalTsmDbOptions; private server: TsmdbServer | null = null; - private client: MongoClient | null = null; + private generatedSocketPath: string | null = null; constructor(options: ILocalTsmDbOptions) { this.options = options; } /** - * Find an available port starting from the given port + * Generate a unique socket path in /tmp */ - private async findAvailablePort(startPort = 27017): Promise { - return new Promise((resolve, reject) => { - const server = plugins.net.createServer(); - server.listen(startPort, '127.0.0.1', () => { - const addr = server.address(); - const port = typeof addr === 'object' && addr ? addr.port : startPort; - server.close(() => resolve(port)); - }); - server.on('error', () => { - this.findAvailablePort(startPort + 1).then(resolve).catch(reject); - }); - }); + private generateSocketPath(): string { + const randomId = crypto.randomBytes(8).toString('hex'); + return path.join(os.tmpdir(), `smartmongo-${randomId}.sock`); } /** - * Start the local TsmDB server and return a connected MongoDB client + * Start the local TsmDB server and return connection info */ - async start(): Promise { - if (this.server && this.client) { + async start(): Promise { + if (this.server) { throw new Error('LocalTsmDb is already running'); } - const port = this.options.port ?? await this.findAvailablePort(); - const host = this.options.host ?? '127.0.0.1'; + // Use provided socket path or generate one + this.generatedSocketPath = this.options.socketPath ?? this.generateSocketPath(); this.server = new TsmdbServer({ - port, - host, + socketPath: this.generatedSocketPath, storage: 'file', storagePath: this.options.folderPath, }); await this.server.start(); - // Dynamically import mongodb to avoid requiring it as a hard dependency - const mongodb = await import('mongodb'); - this.client = new mongodb.MongoClient(this.server.getConnectionUri(), { - directConnection: true, - serverSelectionTimeoutMS: 5000, - }); - await this.client.connect(); - - return this.client; + return { + socketPath: this.generatedSocketPath, + connectionUri: this.server.getConnectionUri(), + }; } /** - * Get the MongoDB client (throws if not started) + * Get connection info (throws if not started) */ - getClient(): MongoClient { - if (!this.client) { + getConnectionInfo(): ILocalTsmDbConnectionInfo { + if (!this.server || !this.generatedSocketPath) { throw new Error('LocalTsmDb is not running. Call start() first.'); } - return this.client; + return { + socketPath: this.generatedSocketPath, + connectionUri: this.server.getConnectionUri(), + }; } /** @@ -123,16 +131,13 @@ export class LocalTsmDb { } /** - * Stop the local TsmDB server and close the client connection + * Stop the local TsmDB server */ async stop(): Promise { - if (this.client) { - await this.client.close(); - this.client = null; - } if (this.server) { await this.server.stop(); this.server = null; + this.generatedSocketPath = null; } } } diff --git a/ts/ts_local/index.ts b/ts/ts_local/index.ts index 838eef8..b4c38bd 100644 --- a/ts/ts_local/index.ts +++ b/ts/ts_local/index.ts @@ -1,2 +1,2 @@ export { LocalTsmDb } from './classes.localtsmdb.js'; -export type { ILocalTsmDbOptions } from './classes.localtsmdb.js'; +export type { ILocalTsmDbOptions, ILocalTsmDbConnectionInfo } from './classes.localtsmdb.js'; diff --git a/ts/ts_local/plugins.ts b/ts/ts_local/plugins.ts index aed316b..3b42c76 100644 --- a/ts/ts_local/plugins.ts +++ b/ts/ts_local/plugins.ts @@ -1,4 +1,3 @@ import * as smartpromise from '@push.rocks/smartpromise'; -import * as net from 'net'; -export { smartpromise, net }; +export { smartpromise }; diff --git a/ts/ts_tsmdb/server/TsmdbServer.ts b/ts/ts_tsmdb/server/TsmdbServer.ts index 0c352c3..99aca82 100644 --- a/ts/ts_tsmdb/server/TsmdbServer.ts +++ b/ts/ts_tsmdb/server/TsmdbServer.ts @@ -1,4 +1,5 @@ import * as net from 'net'; +import * as fs from 'fs/promises'; import * as plugins from '../plugins.js'; import { WireProtocol, OP_QUERY } from './WireProtocol.js'; import { CommandRouter } from './CommandRouter.js'; @@ -10,10 +11,12 @@ import type { IStorageAdapter } from '../storage/IStorageAdapter.js'; * Server configuration options */ export interface ITsmdbServerOptions { - /** Port to listen on (default: 27017) */ + /** Port to listen on (default: 27017) - ignored if socketPath is set */ port?: number; - /** Host to bind to (default: 127.0.0.1) */ + /** Host to bind to (default: 127.0.0.1) - ignored if socketPath is set */ host?: string; + /** Unix socket path - if set, server listens on socket instead of TCP */ + socketPath?: string; /** Storage type: 'memory' or 'file' (default: 'memory') */ storage?: 'memory' | 'file'; /** Path for file storage (required if storage is 'file') */ @@ -54,7 +57,7 @@ interface IConnectionState { * ``` */ export class TsmdbServer { - private options: Required; + private options: Required> & { socketPath: string }; private server: net.Server | null = null; private storage: IStorageAdapter; private commandRouter: CommandRouter; @@ -62,11 +65,14 @@ export class TsmdbServer { private connectionIdCounter = 0; private isRunning = false; private startTime: Date = new Date(); + private useSocket: boolean; constructor(options: ITsmdbServerOptions = {}) { + this.useSocket = !!options.socketPath; this.options = { port: options.port ?? 27017, host: options.host ?? '127.0.0.1', + socketPath: options.socketPath ?? '', storage: options.storage ?? 'memory', storagePath: options.storagePath ?? './data', persistPath: options.persistPath ?? '', @@ -119,6 +125,18 @@ export class TsmdbServer { // Initialize storage await this.storage.initialize(); + // Clean up stale socket file if using Unix socket + if (this.useSocket && this.options.socketPath) { + try { + await fs.unlink(this.options.socketPath); + } catch (err: any) { + // Ignore ENOENT (file doesn't exist) + if (err.code !== 'ENOENT') { + throw err; + } + } + } + return new Promise((resolve, reject) => { this.server = net.createServer((socket) => { this.handleConnection(socket); @@ -132,11 +150,21 @@ export class TsmdbServer { } }); - this.server.listen(this.options.port, this.options.host, () => { - this.isRunning = true; - this.startTime = new Date(); - resolve(); - }); + if (this.useSocket && this.options.socketPath) { + // Listen on Unix socket + this.server.listen(this.options.socketPath, () => { + this.isRunning = true; + this.startTime = new Date(); + resolve(); + }); + } else { + // Listen on TCP + this.server.listen(this.options.port, this.options.host, () => { + this.isRunning = true; + this.startTime = new Date(); + resolve(); + }); + } }); } @@ -161,9 +189,22 @@ export class TsmdbServer { await this.storage.close(); return new Promise((resolve) => { - this.server!.close(() => { + this.server!.close(async () => { this.isRunning = false; this.server = null; + + // Clean up socket file if using Unix socket + if (this.useSocket && this.options.socketPath) { + try { + await fs.unlink(this.options.socketPath); + } catch (err: any) { + // Ignore ENOENT (file doesn't exist) + if (err.code !== 'ENOENT') { + console.error('Failed to remove socket file:', err); + } + } + } + resolve(); }); }); @@ -275,9 +316,21 @@ export class TsmdbServer { * Get the connection URI for this server */ getConnectionUri(): string { + if (this.useSocket && this.options.socketPath) { + // URL-encode the socket path (replace / with %2F) + const encodedPath = encodeURIComponent(this.options.socketPath); + return `mongodb://${encodedPath}`; + } return `mongodb://${this.options.host}:${this.options.port}`; } + /** + * Get the socket path (if using Unix socket mode) + */ + get socketPath(): string | undefined { + return this.useSocket ? this.options.socketPath : undefined; + } + /** * Check if the server is running */