BREAKING CHANGE(localtsmdb): add Unix socket support and change LocalTsmDb API to return connection info instead of a MongoClient
This commit is contained in:
11
changelog.md
11
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
|
||||
|
||||
|
||||
63
readme.md
63
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
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -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<number> {
|
||||
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<MongoClient> {
|
||||
if (this.server && this.client) {
|
||||
async start(): Promise<ILocalTsmDbConnectionInfo> {
|
||||
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<void> {
|
||||
if (this.client) {
|
||||
await this.client.close();
|
||||
this.client = null;
|
||||
}
|
||||
if (this.server) {
|
||||
await this.server.stop();
|
||||
this.server = null;
|
||||
this.generatedSocketPath = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as net from 'net';
|
||||
|
||||
export { smartpromise, net };
|
||||
export { smartpromise };
|
||||
|
||||
@@ -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<ITsmdbServerOptions>;
|
||||
private options: Required<Omit<ITsmdbServerOptions, 'socketPath'>> & { 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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user