diff --git a/js/.changeset/storage-backend-interface.md b/js/.changeset/storage-backend-interface.md new file mode 100644 index 0000000..738a40d --- /dev/null +++ b/js/.changeset/storage-backend-interface.md @@ -0,0 +1,11 @@ +--- +'links-queue-js': minor +--- + +Add pluggable StorageBackend interface for switching between storage backends via configuration + +- Add `StorageBackend` interface with lifecycle, CRUD, batch, and metadata operations +- Add `BackendCapabilities` and `BackendStats` types for backend introspection +- Add `MemoryBackendAdapter` wrapping `MemoryLinkStore` with `StorageBackend` interface +- Add `BackendRegistry` for registering and creating backends by configuration +- Add comprehensive tests for backend registry and adapter diff --git a/js/src/backends/registry.d.ts b/js/src/backends/registry.d.ts new file mode 100644 index 0000000..d7bcab1 --- /dev/null +++ b/js/src/backends/registry.d.ts @@ -0,0 +1,78 @@ +/** + * Backend registry type definitions for links-queue. + */ + +import type { + StorageBackend, + BackendOptions, + BackendConstructor, + BackendFactory, + BackendCapabilities, + BackendStats, + MemoryBackendOptions, +} from './types.ts'; +import type { Link, LinkId, LinkPattern } from '../types.ts'; + +/** + * Memory backend adapter that wraps MemoryLinkStore with StorageBackend interface. + */ +export declare class MemoryBackendAdapter implements StorageBackend { + constructor(options?: MemoryBackendOptions); + + connect(): Promise; + disconnect(): Promise; + isConnected(): boolean; + + save(link: Link): Promise; + load(id: LinkId): Promise; + delete(id: LinkId): Promise; + query(pattern: LinkPattern): Promise; + + saveBatch(links: readonly Link[]): Promise; + deleteBatch(ids: readonly LinkId[]): Promise; + + getCapabilities(): BackendCapabilities; + getStats(): BackendStats; + + clear(): Promise; +} + +/** + * Backend registry interface. + */ +export interface BackendRegistryInterface { + /** + * Registers a backend implementation. + */ + register(name: string, backend: BackendConstructor | BackendFactory): void; + + /** + * Unregisters a backend implementation. + */ + unregister(name: string): boolean; + + /** + * Checks if a backend is registered. + */ + has(name: string): boolean; + + /** + * Creates a backend instance from configuration. + */ + create(config: BackendOptions): StorageBackend; + + /** + * Lists all registered backend types. + */ + listTypes(): string[]; + + /** + * Resets the registry to default state. + */ + reset(): void; +} + +/** + * Singleton backend registry instance. + */ +export declare const BackendRegistry: BackendRegistryInterface; diff --git a/js/src/backends/registry.js b/js/src/backends/registry.js new file mode 100644 index 0000000..e2b9eef --- /dev/null +++ b/js/src/backends/registry.js @@ -0,0 +1,435 @@ +/** + * Backend registry for links-queue. + * + * This module provides a registry for storage backend implementations, + * allowing backends to be registered and instantiated by name via configuration. + * + * @module backends/registry + * + * @example + * // Register a custom backend + * BackendRegistry.register('my-custom', MyCustomBackend); + * + * // Create backend via configuration + * const backend = BackendRegistry.create({ + * type: 'my-custom', + * options: { /* custom options *\/ } + * }); + */ + +import { MemoryLinkStore } from './memory.js'; + +/** + * A wrapper that adapts MemoryLinkStore to the StorageBackend interface. + * + * This allows MemoryLinkStore to be used as a storage backend while maintaining + * backward compatibility with its existing interface. + * + * @implements {import('./types.ts').StorageBackend} + */ +class MemoryBackendAdapter { + /** + * Creates a new MemoryBackendAdapter. + * + * @param {import('./types.ts').MemoryBackendOptions} [options] - Configuration options + */ + constructor(options = {}) { + /** @private */ + this._store = new MemoryLinkStore(); + + /** @private */ + this._options = options; + + /** @private */ + this._connected = false; + + /** @private */ + this._connectedAt = null; + + /** @private */ + this._stats = { + reads: 0, + writes: 0, + deletes: 0, + queries: 0, + }; + } + + /** + * Establishes connection to the backend. + * For memory backend, this is essentially a no-op but tracks connection state. + * + * @returns {Promise} + */ + // eslint-disable-next-line require-await + async connect() { + if (this._connected) { + return; + } + this._connected = true; + this._connectedAt = new Date().toISOString(); + } + + /** + * Disconnects from the backend. + * For memory backend, this clears the connection state. + * + * @returns {Promise} + */ + // eslint-disable-next-line require-await + async disconnect() { + this._connected = false; + this._connectedAt = null; + } + + /** + * Checks if the backend is connected. + * + * @returns {boolean} + */ + isConnected() { + return this._connected; + } + + /** + * Saves a link to storage. + * + * @param {import('../index.js').Link} link - The link to save + * @returns {Promise} + */ + async save(link) { + this._ensureConnected(); + this._stats.writes++; + + // If the link has an ID of 0 or is "nothing", create a new link + if (link.id === 0 || link.id === '' || link.id === 0n) { + const created = link.values + ? await this._store.createWithValues( + link.source, + link.target, + link.values + ) + : await this._store.create(link.source, link.target); + return created.id; + } + + // Otherwise, update existing or create with the given ID + // For memory backend, we'll try to update first, then create if not exists + const existing = await this._store.get(link.id); + if (existing) { + const updated = await this._store.update( + link.id, + link.source, + link.target + ); + return updated.id; + } + + // Create new link (will use auto-generated ID) + const created = link.values + ? await this._store.createWithValues( + link.source, + link.target, + link.values + ) + : await this._store.create(link.source, link.target); + return created.id; + } + + /** + * Loads a link by ID. + * + * @param {import('../index.js').LinkId} id - The link ID + * @returns {Promise} + */ + // eslint-disable-next-line require-await + async load(id) { + this._ensureConnected(); + this._stats.reads++; + return this._store.get(id); + } + + /** + * Deletes a link by ID. + * + * @param {import('../index.js').LinkId} id - The link ID + * @returns {Promise} + */ + // eslint-disable-next-line require-await + async delete(id) { + this._ensureConnected(); + this._stats.deletes++; + return this._store.delete(id); + } + + /** + * Queries links by pattern. + * + * @param {import('../index.js').LinkPattern} pattern - The pattern to match + * @returns {Promise} + */ + // eslint-disable-next-line require-await + async query(pattern) { + this._ensureConnected(); + this._stats.queries++; + return this._store.find(pattern); + } + + /** + * Saves multiple links in batch. + * + * @param {readonly import('../index.js').Link[]} links - Links to save + * @returns {Promise} + */ + async saveBatch(links) { + this._ensureConnected(); + const ids = []; + for (const link of links) { + const id = await this.save(link); + ids.push(id); + } + return ids; + } + + /** + * Deletes multiple links in batch. + * + * @param {readonly import('../index.js').LinkId[]} ids - IDs to delete + * @returns {Promise} + */ + async deleteBatch(ids) { + this._ensureConnected(); + const results = []; + for (const id of ids) { + const result = await this.delete(id); + results.push(result); + } + return results; + } + + /** + * Returns backend capabilities. + * + * @returns {import('./types.ts').BackendCapabilities} + */ + getCapabilities() { + return { + supportsTransactions: false, + supportsBatchOperations: false, // We simulate batch ops + durabilityLevel: 'none', + maxLinkSize: 0, // Unlimited + supportsPatternQueries: true, + }; + } + + /** + * Returns backend statistics. + * + * @returns {import('./types.ts').BackendStats} + */ + getStats() { + const now = Date.now(); + const connectedTime = this._connectedAt + ? new Date(this._connectedAt).getTime() + : now; + + return { + totalLinks: this._store._links.size, + usedSpace: 0, // Not tracked for memory backend + operations: { ...this._stats }, + connectedAt: this._connectedAt, + uptimeMs: this._connected ? now - connectedTime : 0, + }; + } + + /** + * Ensures the backend is connected before operations. + * + * @private + * @throws {Error} If not connected + */ + _ensureConnected() { + if (!this._connected) { + throw new Error('Backend is not connected. Call connect() first.'); + } + } + + /** + * Clears all data (for testing). + * + * @returns {Promise} + */ + async clear() { + await this._store.clear(); + this._stats = { + reads: 0, + writes: 0, + deletes: 0, + queries: 0, + }; + } +} + +/** + * Registry for storage backend implementations. + * + * Provides methods to register custom backends and create backend instances + * from configuration. + * + * @example + * // Register a custom backend + * BackendRegistry.register('redis', RedisBackend); + * + * // Create from config + * const backend = BackendRegistry.create({ type: 'redis', options: { host: 'localhost' } }); + */ +class BackendRegistryClass { + constructor() { + /** + * Map of backend type names to constructors/factories. + * @private + * @type {Map} + */ + this._backends = new Map(); + + // Register built-in backends + this._backends.set('memory', MemoryBackendAdapter); + } + + /** + * Registers a backend implementation. + * + * @param {string} name - The name to register the backend under + * @param {import('./types.ts').BackendConstructor | import('./types.ts').BackendFactory} backend - The backend constructor or factory + * @throws {Error} If name is empty or backend is invalid + * + * @example + * // Register with constructor + * BackendRegistry.register('my-backend', MyBackend); + * + * // Register with factory + * BackendRegistry.register('complex-backend', (opts) => new ComplexBackend(opts)); + */ + register(name, backend) { + if (!name || typeof name !== 'string') { + throw new Error('Backend name must be a non-empty string'); + } + + if (typeof backend !== 'function') { + throw new Error('Backend must be a constructor or factory function'); + } + + this._backends.set(name, backend); + } + + /** + * Unregisters a backend implementation. + * + * @param {string} name - The name of the backend to unregister + * @returns {boolean} True if the backend was unregistered, false if it wasn't registered + * + * @example + * BackendRegistry.unregister('my-backend'); + */ + unregister(name) { + return this._backends.delete(name); + } + + /** + * Checks if a backend is registered. + * + * @param {string} name - The backend name to check + * @returns {boolean} True if registered + * + * @example + * if (BackendRegistry.has('redis')) { + * const backend = BackendRegistry.create({ type: 'redis' }); + * } + */ + has(name) { + return this._backends.has(name); + } + + /** + * Creates a backend instance from configuration. + * + * @param {import('./types.ts').BackendOptions} config - Backend configuration + * @returns {import('./types.ts').StorageBackend} The created backend instance + * @throws {Error} If the backend type is not registered + * + * @example + * const memoryBackend = BackendRegistry.create({ type: 'memory' }); + * + * const customBackend = BackendRegistry.create({ + * type: 'my-custom', + * options: { host: 'localhost', port: 6379 } + * }); + */ + create(config) { + const { type, options } = config; + + const Backend = this._backends.get(type); + if (!Backend) { + throw new Error( + `Unknown backend type: "${type}". ` + + `Available types: ${this.listTypes().join(', ')}` + ); + } + + // Try to instantiate - could be constructor or factory + try { + // Check if it's a class (has prototype with constructor) + if (Backend.prototype && Backend.prototype.constructor === Backend) { + return new Backend(options); + } + // Otherwise it's a factory function + return Backend(options); + } catch (error) { + throw new Error(`Failed to create backend "${type}": ${error.message}`); + } + } + + /** + * Lists all registered backend types. + * + * @returns {string[]} Array of registered backend type names + * + * @example + * const types = BackendRegistry.listTypes(); + * console.log(types); // ['memory', 'my-custom', ...] + */ + listTypes() { + return Array.from(this._backends.keys()); + } + + /** + * Resets the registry to default state (only built-in backends). + * + * Primarily useful for testing. + */ + reset() { + this._backends.clear(); + this._backends.set('memory', MemoryBackendAdapter); + } +} + +/** + * Singleton instance of the backend registry. + * + * @type {BackendRegistryClass} + * + * @example + * import { BackendRegistry } from 'links-queue'; + * + * // Register custom backend + * BackendRegistry.register('redis', RedisBackend); + * + * // Create backend + * const backend = BackendRegistry.create({ type: 'redis' }); + */ +export const BackendRegistry = new BackendRegistryClass(); + +/** + * The memory backend adapter class (exported for direct use). + */ +export { MemoryBackendAdapter }; diff --git a/js/src/backends/types.ts b/js/src/backends/types.ts new file mode 100644 index 0000000..850cf4c --- /dev/null +++ b/js/src/backends/types.ts @@ -0,0 +1,405 @@ +/** + * Storage backend type definitions for links-queue. + * + * This module defines the StorageBackend interface that enables pluggable + * storage backends. Implementations can provide different tradeoffs between + * performance, durability, and features. + * + * @module backends/types + * + * @see ARCHITECTURE.md - Storage Backend Layer + * @see REQUIREMENTS.md - REQ-STORE-001 through REQ-STORE-022 + */ + +import type { Link, LinkId, LinkPattern, LinkRef } from '../types.ts'; + +// ============================================================================= +// Backend Capabilities +// ============================================================================= + +/** + * Durability level supported by a storage backend. + * + * - `none`: No durability guarantees (in-memory only) + * - `fsync`: Data is synced to disk + * - `replicated`: Data is replicated across multiple nodes + */ +export type DurabilityLevel = 'none' | 'fsync' | 'replicated'; + +/** + * Describes the capabilities of a storage backend. + * + * This allows the system to validate that a backend meets the requirements + * for a given operating mode and to adapt behavior accordingly. + * + * @example + * const capabilities = backend.getCapabilities(); + * if (capabilities.supportsTransactions) { + * // Use transactional operations + * } + */ +export interface BackendCapabilities { + /** + * Whether the backend supports atomic transactions. + */ + readonly supportsTransactions: boolean; + + /** + * Whether the backend supports batch operations natively. + * If false, batch operations are simulated with individual operations. + */ + readonly supportsBatchOperations: boolean; + + /** + * The durability level provided by this backend. + */ + readonly durabilityLevel: DurabilityLevel; + + /** + * Maximum size of a single link in bytes (0 for unlimited). + */ + readonly maxLinkSize: number; + + /** + * Whether the backend supports pattern-based queries natively. + * If false, queries are performed by scanning all links. + */ + readonly supportsPatternQueries: boolean; +} + +// ============================================================================= +// Backend Statistics +// ============================================================================= + +/** + * Operation counters for backend statistics. + */ +export interface OperationStats { + /** + * Number of read operations performed. + */ + readonly reads: number; + + /** + * Number of write operations performed (save/update). + */ + readonly writes: number; + + /** + * Number of delete operations performed. + */ + readonly deletes: number; + + /** + * Number of query operations performed. + */ + readonly queries: number; +} + +/** + * Statistics and metrics for a storage backend. + * + * @example + * const stats = backend.getStats(); + * console.log(`Total links: ${stats.totalLinks}`); + * console.log(`Used space: ${stats.usedSpace} bytes`); + */ +export interface BackendStats { + /** + * Total number of links stored. + */ + readonly totalLinks: number; + + /** + * Approximate storage space used in bytes. + */ + readonly usedSpace: number; + + /** + * Operation counters. + */ + readonly operations: OperationStats; + + /** + * Timestamp when the backend was connected (ISO string). + */ + readonly connectedAt: string | null; + + /** + * Uptime in milliseconds since connection. + */ + readonly uptimeMs: number; +} + +// ============================================================================= +// Storage Backend Interface +// ============================================================================= + +/** + * Interface for pluggable storage backends. + * + * This abstraction allows switching between memory, link-cli, and custom + * backends via configuration without code changes. + * + * Following [code-architecture-principles](https://github.com/link-foundation/code-architecture-principles): + * - **Interfaces & Abstraction**: Hide implementation details behind stable contracts + * - **Configuration Over Code**: Scale from embedded to distributed without code changes + * + * @example + * // Create a backend + * const backend = new MemoryBackend(); + * + * // Connect and use + * await backend.connect(); + * + * // Save a link + * const id = await backend.save({ id: 0, source: 1, target: 2 }); + * + * // Load it back + * const link = await backend.load(id); + * + * // Query by pattern + * const links = await backend.query({ source: 1 }); + * + * // Disconnect when done + * await backend.disconnect(); + */ +export interface StorageBackend { + // --------------------------------------------------------------------------- + // Lifecycle Operations + // --------------------------------------------------------------------------- + + /** + * Establishes a connection to the storage backend. + * + * This should be called before any other operations. For in-memory backends, + * this may be a no-op but should still be called for consistency. + * + * @throws If connection fails + * + * @example + * const backend = new LinkCliBackend({ path: './data/db.links' }); + * await backend.connect(); + */ + connect(): Promise; + + /** + * Gracefully disconnects from the storage backend. + * + * This should flush any pending writes and release resources. + * + * @example + * await backend.disconnect(); + */ + disconnect(): Promise; + + /** + * Checks if the backend is currently connected. + * + * @returns True if connected and ready for operations + * + * @example + * if (!backend.isConnected()) { + * await backend.connect(); + * } + */ + isConnected(): boolean; + + // --------------------------------------------------------------------------- + // Core Operations + // --------------------------------------------------------------------------- + + /** + * Saves a link to storage. + * + * If the link's ID is 0 or represents "nothing", a new ID will be assigned. + * If the link already exists (by structure, for deduplication), the existing + * ID may be returned depending on the backend's deduplication strategy. + * + * @param link - The link to save + * @returns The ID of the saved link (may be new or existing) + * + * @example + * const id = await backend.save({ id: 0, source: 1, target: 2 }); + * console.log(`Saved link with ID: ${id}`); + */ + save(link: Link): Promise; + + /** + * Loads a link by its ID. + * + * @param id - The ID of the link to load + * @returns The link if found, null otherwise + * + * @example + * const link = await backend.load(42); + * if (link) { + * console.log(`Link: ${link.source} -> ${link.target}`); + * } + */ + load(id: LinkId): Promise; + + /** + * Deletes a link by its ID. + * + * @param id - The ID of the link to delete + * @returns True if the link was deleted, false if it didn't exist + * + * @example + * const deleted = await backend.delete(42); + * if (deleted) { + * console.log('Link deleted'); + * } + */ + delete(id: LinkId): Promise; + + /** + * Queries links matching a pattern. + * + * @param pattern - The pattern to match + * @returns Array of matching links + * + * @example + * // Find all links with source = 5 + * const links = await backend.query({ source: 5 }); + */ + query(pattern: LinkPattern): Promise; + + // --------------------------------------------------------------------------- + // Batch Operations + // --------------------------------------------------------------------------- + + /** + * Saves multiple links in a batch. + * + * For backends that support native batch operations, this is more efficient + * than calling save() repeatedly. For others, it falls back to individual saves. + * + * @param links - Array of links to save + * @returns Array of IDs (in the same order as input) + * + * @example + * const ids = await backend.saveBatch([ + * { id: 0, source: 1, target: 2 }, + * { id: 0, source: 3, target: 4 }, + * ]); + */ + saveBatch(links: readonly Link[]): Promise; + + /** + * Deletes multiple links by their IDs. + * + * @param ids - Array of IDs to delete + * @returns Array of booleans indicating success for each deletion (in order) + * + * @example + * const results = await backend.deleteBatch([1, 2, 3]); + * const deletedCount = results.filter(Boolean).length; + */ + deleteBatch(ids: readonly LinkId[]): Promise; + + // --------------------------------------------------------------------------- + // Metadata Operations + // --------------------------------------------------------------------------- + + /** + * Returns the capabilities of this backend. + * + * @returns Backend capabilities descriptor + * + * @example + * const caps = backend.getCapabilities(); + * if (caps.durabilityLevel === 'none') { + * console.warn('Data will be lost on restart'); + * } + */ + getCapabilities(): BackendCapabilities; + + /** + * Returns statistics about the backend. + * + * @returns Current backend statistics + * + * @example + * const stats = backend.getStats(); + * console.log(`Total links: ${stats.totalLinks}`); + */ + getStats(): BackendStats; +} + +// ============================================================================= +// Backend Configuration +// ============================================================================= + +/** + * Configuration options for memory backend. + */ +export interface MemoryBackendOptions { + /** + * Initial capacity for the link storage (hint for pre-allocation). + */ + initialCapacity?: number; + + /** + * Whether to enable link deduplication. + * @default true + */ + deduplication?: boolean; +} + +/** + * Configuration options for link-cli backend. + */ +export interface LinkCliBackendOptions { + /** + * Path to the SQLite database file. + */ + path: string; + + /** + * Whether to create the database if it doesn't exist. + * @default true + */ + createIfMissing?: boolean; + + /** + * Whether to enable write-ahead logging (WAL) mode. + * @default true + */ + walMode?: boolean; +} + +/** + * Generic backend configuration for custom backends. + */ +export interface CustomBackendOptions { + [key: string]: unknown; +} + +/** + * Union type for all backend configuration options. + */ +export type BackendOptions = + | { type: 'memory'; options?: MemoryBackendOptions } + | { type: 'link-cli'; options: LinkCliBackendOptions } + | { type: string; options?: CustomBackendOptions }; + +// ============================================================================= +// Backend Factory Types +// ============================================================================= + +/** + * Constructor type for storage backends. + * + * Used by the backend registry for creating backend instances. + */ +export type BackendConstructor = new ( + options?: CustomBackendOptions +) => StorageBackend; + +/** + * Factory function type for creating storage backends. + * + * Can be used instead of a constructor for more complex initialization. + */ +export type BackendFactory = (options?: CustomBackendOptions) => StorageBackend; diff --git a/js/src/index.d.ts b/js/src/index.d.ts index 5712518..f946d99 100644 --- a/js/src/index.d.ts +++ b/js/src/index.d.ts @@ -166,6 +166,28 @@ export declare const delay: (ms: number) => Promise; export { MemoryLinkStore } from './backends/memory.d.ts'; +// Storage backend interface and registry +export type { + StorageBackend, + BackendCapabilities, + BackendStats, + OperationStats, + DurabilityLevel, + BackendOptions, + MemoryBackendOptions, + LinkCliBackendOptions, + CustomBackendOptions, + BackendConstructor, + BackendFactory, +} from './backends/types.ts'; + +export { + BackendRegistry, + MemoryBackendAdapter, +} from './backends/registry.d.ts'; + +export type { BackendRegistryInterface } from './backends/registry.d.ts'; + // ============================================================================= // Queue Exports // ============================================================================= diff --git a/js/src/index.js b/js/src/index.js index 67b6b86..a0ac667 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -176,6 +176,9 @@ export const delay = (ms) => export { MemoryLinkStore } from './backends/memory.js'; +// Storage backend interface and registry +export { BackendRegistry, MemoryBackendAdapter } from './backends/registry.js'; + // ============================================================================= // Queue Exports // ============================================================================= diff --git a/js/src/types.ts b/js/src/types.ts index 118f4df..484522e 100644 --- a/js/src/types.ts +++ b/js/src/types.ts @@ -413,4 +413,7 @@ export type LinkResult = // Re-exports for convenience // ============================================================================= -export type { Link as ILink } from './types.js'; +/** + * @deprecated Use Link instead. ILink is provided for backward compatibility. + */ +export type ILink = Link; diff --git a/js/tests/backend-registry.test.js b/js/tests/backend-registry.test.js new file mode 100644 index 0000000..4f6cc80 --- /dev/null +++ b/js/tests/backend-registry.test.js @@ -0,0 +1,321 @@ +/** + * Tests for BackendRegistry and MemoryBackendAdapter + * + * Note: This test avoids beforeEach for Deno compatibility. + * Deno's node:test compatibility layer doesn't support beforeEach. + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + BackendRegistry, + MemoryBackendAdapter, + createLink, +} from '../src/index.js'; + +/** + * Creates a connected backend for testing. + * @returns {Promise} + */ +async function createConnectedBackend() { + const backend = new MemoryBackendAdapter(); + await backend.connect(); + return backend; +} + +// eslint-disable-next-line max-lines-per-function +describe('MemoryBackendAdapter', () => { + describe('lifecycle', () => { + it('should start disconnected', () => { + const backend = new MemoryBackendAdapter(); + assert.strictEqual(backend.isConnected(), false); + }); + + it('should connect and disconnect', async () => { + const backend = new MemoryBackendAdapter(); + + await backend.connect(); + assert.strictEqual(backend.isConnected(), true); + + await backend.disconnect(); + assert.strictEqual(backend.isConnected(), false); + }); + + it('should track connection time in stats', async () => { + const backend = new MemoryBackendAdapter(); + const statsBefore = backend.getStats(); + assert.strictEqual(statsBefore.connectedAt, null); + + await backend.connect(); + const statsAfter = backend.getStats(); + assert.ok(statsAfter.connectedAt !== null); + // connectedAt is stored as ISO string + assert.ok(typeof statsAfter.connectedAt === 'string'); + assert.ok(new Date(statsAfter.connectedAt).getTime() > 0); + }); + }); + + describe('CRUD operations', () => { + it('should save a link and return ID', async () => { + const backend = await createConnectedBackend(); + const link = createLink(0, 1, 2); + const id = await backend.save(link); + assert.ok(id !== 0); + }); + + it('should load a saved link', async () => { + const backend = await createConnectedBackend(); + const link = createLink(0, 1, 2); + const id = await backend.save(link); + + const loaded = await backend.load(id); + assert.ok(loaded !== null); + assert.strictEqual(loaded.source, 1); + assert.strictEqual(loaded.target, 2); + }); + + it('should return null for non-existent link', async () => { + const backend = await createConnectedBackend(); + const loaded = await backend.load(999); + assert.strictEqual(loaded, null); + }); + + it('should delete a link', async () => { + const backend = await createConnectedBackend(); + const link = createLink(0, 1, 2); + const id = await backend.save(link); + + const deleted = await backend.delete(id); + assert.strictEqual(deleted, true); + + const loaded = await backend.load(id); + assert.strictEqual(loaded, null); + }); + + it('should return false when deleting non-existent link', async () => { + const backend = await createConnectedBackend(); + const deleted = await backend.delete(999); + assert.strictEqual(deleted, false); + }); + + it('should query links by pattern', async () => { + const backend = await createConnectedBackend(); + await backend.save(createLink(0, 1, 2)); + await backend.save(createLink(0, 1, 3)); + await backend.save(createLink(0, 2, 3)); + + const results = await backend.query({ source: 1 }); + assert.strictEqual(results.length, 2); + }); + + it('should query all links with empty pattern', async () => { + const backend = await createConnectedBackend(); + await backend.save(createLink(0, 1, 2)); + await backend.save(createLink(0, 3, 4)); + + const results = await backend.query({}); + assert.strictEqual(results.length, 2); + }); + }); + + describe('batch operations', () => { + it('should save multiple links in batch', async () => { + const backend = await createConnectedBackend(); + const links = [ + createLink(0, 1, 2), + createLink(0, 3, 4), + createLink(0, 5, 6), + ]; + + const ids = await backend.saveBatch(links); + assert.strictEqual(ids.length, 3); + assert.ok(ids.every((id) => id !== 0)); + }); + + it('should delete multiple links in batch', async () => { + const backend = await createConnectedBackend(); + const id1 = await backend.save(createLink(0, 1, 2)); + const id2 = await backend.save(createLink(0, 3, 4)); + + const results = await backend.deleteBatch([id1, id2, 999]); + assert.deepStrictEqual(results, [true, true, false]); + }); + }); + + describe('capabilities and stats', () => { + it('should return correct capabilities', () => { + const backend = new MemoryBackendAdapter(); + const caps = backend.getCapabilities(); + + assert.strictEqual(caps.supportsTransactions, false); + assert.strictEqual(caps.supportsBatchOperations, false); + assert.strictEqual(caps.durabilityLevel, 'none'); + assert.strictEqual(caps.maxLinkSize, 0); + assert.strictEqual(caps.supportsPatternQueries, true); + }); + + it('should track operation statistics', async () => { + const backend = await createConnectedBackend(); + + const id = await backend.save(createLink(0, 1, 2)); + await backend.load(id); + await backend.query({}); + await backend.delete(id); + + const stats = backend.getStats(); + assert.strictEqual(stats.operations.writes, 1); + assert.strictEqual(stats.operations.reads, 1); + assert.strictEqual(stats.operations.queries, 1); + assert.strictEqual(stats.operations.deletes, 1); + }); + + it('should track total links count', async () => { + const backend = await createConnectedBackend(); + + await backend.save(createLink(0, 1, 2)); + await backend.save(createLink(0, 3, 4)); + + const stats = backend.getStats(); + assert.strictEqual(stats.totalLinks, 2); + }); + }); + + describe('clear', () => { + it('should clear all links', async () => { + const backend = await createConnectedBackend(); + + await backend.save(createLink(0, 1, 2)); + await backend.save(createLink(0, 3, 4)); + + await backend.clear(); + + const stats = backend.getStats(); + assert.strictEqual(stats.totalLinks, 0); + }); + }); + + describe('error handling', () => { + it('should throw when saving without connection', async () => { + const backend = new MemoryBackendAdapter(); + // Not connected + + await assert.rejects(async () => { + await backend.save(createLink(0, 1, 2)); + }, /not connected/i); + }); + + it('should throw when loading without connection', async () => { + const backend = new MemoryBackendAdapter(); + + await assert.rejects(async () => { + await backend.load(1); + }, /not connected/i); + }); + + it('should throw when deleting without connection', async () => { + const backend = new MemoryBackendAdapter(); + + await assert.rejects(async () => { + await backend.delete(1); + }, /not connected/i); + }); + + it('should throw when querying without connection', async () => { + const backend = new MemoryBackendAdapter(); + + await assert.rejects(async () => { + await backend.query({}); + }, /not connected/i); + }); + }); +}); + +describe('BackendRegistry', () => { + // Note: Each test resets the registry to ensure clean state + // This replaces beforeEach for Deno compatibility + + describe('registration', () => { + it('should have memory backend registered by default', () => { + BackendRegistry.reset(); + assert.strictEqual(BackendRegistry.has('memory'), true); + }); + + it('should list registered types', () => { + BackendRegistry.reset(); + const types = BackendRegistry.listTypes(); + assert.ok(types.includes('memory')); + }); + + it('should register a custom backend', () => { + BackendRegistry.reset(); + BackendRegistry.register('custom', () => new MemoryBackendAdapter()); + assert.strictEqual(BackendRegistry.has('custom'), true); + }); + + it('should unregister a backend', () => { + BackendRegistry.reset(); + BackendRegistry.register('custom', () => new MemoryBackendAdapter()); + const result = BackendRegistry.unregister('custom'); + assert.strictEqual(result, true); + assert.strictEqual(BackendRegistry.has('custom'), false); + }); + + it('should return false when unregistering non-existent backend', () => { + BackendRegistry.reset(); + const result = BackendRegistry.unregister('non-existent'); + assert.strictEqual(result, false); + }); + }); + + describe('creation', () => { + it('should create memory backend by default', () => { + BackendRegistry.reset(); + const backend = BackendRegistry.create({ type: 'memory' }); + assert.ok(backend instanceof MemoryBackendAdapter); + }); + + it('should create backend with options', () => { + BackendRegistry.reset(); + const backend = BackendRegistry.create({ + type: 'memory', + options: { initialCapacity: 100 }, + }); + assert.ok(backend instanceof MemoryBackendAdapter); + }); + + it('should throw for unknown backend type', () => { + BackendRegistry.reset(); + assert.throws(() => { + BackendRegistry.create({ type: 'unknown' }); + }, /unknown/i); + }); + + it('should create custom backend', () => { + BackendRegistry.reset(); + let factoryCalled = false; + BackendRegistry.register('custom', (config) => { + factoryCalled = true; + return new MemoryBackendAdapter(config.options); + }); + + const backend = BackendRegistry.create({ + type: 'custom', + options: { initialCapacity: 50 }, + }); + + assert.strictEqual(factoryCalled, true); + assert.ok(backend instanceof MemoryBackendAdapter); + }); + }); + + describe('reset', () => { + it('should restore default state', () => { + BackendRegistry.reset(); + BackendRegistry.register('custom', () => new MemoryBackendAdapter()); + BackendRegistry.reset(); + + assert.strictEqual(BackendRegistry.has('custom'), false); + assert.strictEqual(BackendRegistry.has('memory'), true); + }); + }); +}); diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 1bf52c0..a253220 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "links-queue" -version = "0.1.0" +version = "0.2.0" edition = "2021" description = "A Rust implementation of Links Queue - universal queue system using links" readme = "README.md" diff --git a/rust/package-lock.json b/rust/package-lock.json new file mode 100644 index 0000000..ab15834 --- /dev/null +++ b/rust/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "rust", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/rust/src/backends/memory_backend.rs b/rust/src/backends/memory_backend.rs new file mode 100644 index 0000000..6a518e3 --- /dev/null +++ b/rust/src/backends/memory_backend.rs @@ -0,0 +1,446 @@ +//! In-memory storage backend implementing [`StorageBackend`]. +//! +//! This module provides [`MemoryBackend`], a wrapper around [`MemoryLinkStore`] +//! that implements the [`StorageBackend`] trait for pluggable backend support. +//! +//! # Features +//! +//! - **O(1) lookups**: Uses `HashMap` for constant-time access by ID +//! - **Deduplication**: Identical link structures share the same ID +//! - **Statistics tracking**: Tracks operation counts and connection state +//! +//! # Example +//! +//! ```rust +//! use links_queue::{MemoryBackend, StorageBackend, Link, LinkRef, LinkPattern}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! let mut backend = MemoryBackend::::new(); +//! backend.connect().await?; +//! +//! // Save a link +//! let link = Link::new(0, LinkRef::Id(1), LinkRef::Id(2)); +//! let id = backend.save(link).await?; +//! println!("Saved with ID: {}", id); +//! +//! // Load it back +//! let loaded = backend.load(id).await?; +//! println!("Loaded: {:?}", loaded); +//! +//! backend.disconnect().await?; +//! Ok(()) +//! } +//! ``` + +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::{Link, LinkPattern, LinkStore, LinkType, MemoryLinkStore}; + +use super::traits::{ + BackendCapabilities, BackendError, BackendResult, BackendStats, DurabilityLevel, + OperationStats, StorageBackend, +}; + +// ============================================================================= +// Memory Backend +// ============================================================================= + +/// In-memory storage backend implementing [`StorageBackend`]. +/// +/// This is a wrapper around [`MemoryLinkStore`] that adds connection state +/// tracking, operation statistics, and implements the async [`StorageBackend`] trait. +/// +/// # Type Parameters +/// +/// * `T` - The link ID type (must implement [`LinkType`]) +/// +/// # Thread Safety +/// +/// `MemoryBackend` is `Send + Sync`. For concurrent access, wrap in +/// `tokio::sync::RwLock` or similar synchronization primitive. +/// +/// # Example +/// +/// ```rust +/// use links_queue::{MemoryBackend, StorageBackend, Link, LinkRef}; +/// +/// #[tokio::main] +/// async fn main() { +/// let mut backend = MemoryBackend::::new(); +/// backend.connect().await.unwrap(); +/// +/// let link = Link::new(0, LinkRef::Id(1), LinkRef::Id(2)); +/// let id = backend.save(link).await.unwrap(); +/// +/// let stats = backend.stats(); +/// println!("Total links: {}", stats.total_links); +/// } +/// ``` +#[derive(Debug)] +pub struct MemoryBackend { + /// The underlying link store. + store: MemoryLinkStore, + + /// Whether the backend is connected. + connected: bool, + + /// Timestamp when connected (Unix millis). + connected_at: Option, + + /// Operation statistics. + stats: OperationStats, +} + +impl Default for MemoryBackend { + fn default() -> Self { + Self::new() + } +} + +impl MemoryBackend { + /// Creates a new memory backend. + /// + /// # Example + /// + /// ```rust + /// use links_queue::MemoryBackend; + /// + /// let backend = MemoryBackend::::new(); + /// ``` + #[must_use] + pub fn new() -> Self { + Self { + store: MemoryLinkStore::new(), + connected: false, + connected_at: None, + stats: OperationStats::default(), + } + } + + /// Creates a new memory backend with pre-allocated capacity. + /// + /// # Arguments + /// + /// * `capacity` - Number of links to pre-allocate space for + /// + /// # Example + /// + /// ```rust + /// use links_queue::MemoryBackend; + /// + /// let backend = MemoryBackend::::with_capacity(1000); + /// ``` + #[must_use] + pub fn with_capacity(capacity: usize) -> Self { + Self { + store: MemoryLinkStore::with_capacity(capacity), + connected: false, + connected_at: None, + stats: OperationStats::default(), + } + } + + /// Clears all data from the backend. + /// + /// This resets the store but does not reset statistics or connection state. + /// + /// # Example + /// + /// ```rust + /// use links_queue::{MemoryBackend, StorageBackend, Link, LinkRef}; + /// + /// #[tokio::main] + /// async fn main() { + /// let mut backend = MemoryBackend::::new(); + /// backend.connect().await.unwrap(); + /// + /// backend.save(Link::new(0, LinkRef::Id(1), LinkRef::Id(2))).await.unwrap(); + /// assert_eq!(backend.stats().total_links, 1); + /// + /// backend.clear(); + /// assert_eq!(backend.stats().total_links, 0); + /// } + /// ``` + pub fn clear(&mut self) { + self.store.clear(); + } + + /// Resets the backend to initial state including statistics. + pub fn reset(&mut self) { + self.store.reset(); + self.stats = OperationStats::default(); + } + + /// Gets the current Unix timestamp in milliseconds. + #[allow(clippy::cast_possible_truncation)] + fn current_time_ms() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0) + } + + /// Ensures the backend is connected, returning an error if not. + const fn ensure_connected(&self) -> BackendResult { + if self.connected { + Ok(()) + } else { + Err(BackendError::NotConnected) + } + } +} + +// ============================================================================= +// StorageBackend Implementation +// ============================================================================= + +impl StorageBackend for MemoryBackend { + async fn connect(&mut self) -> BackendResult { + if !self.connected { + self.connected = true; + self.connected_at = Some(Self::current_time_ms()); + } + Ok(()) + } + + async fn disconnect(&mut self) -> BackendResult { + self.connected = false; + self.connected_at = None; + Ok(()) + } + + fn is_connected(&self) -> bool { + self.connected + } + + async fn save(&mut self, link: Link) -> BackendResult { + self.ensure_connected()?; + self.stats.writes += 1; + + // If ID is zero/nothing, create a new link + if link.id.is_nothing() { + let created = if let Some(values) = link.values { + self.store + .create_with_values(link.source, link.target, values)? + } else { + self.store.create(link.source, link.target)? + }; + return Ok(created.id); + } + + // Try to update existing link + if self.store.exists(link.id) { + let updated = self + .store + .update(link.id, link.source.clone(), link.target.clone())?; + return Ok(updated.id); + } + + // Create new link (will get auto-generated ID) + let created = if let Some(values) = link.values { + self.store + .create_with_values(link.source, link.target, values)? + } else { + self.store.create(link.source, link.target)? + }; + Ok(created.id) + } + + async fn load(&self, id: T) -> BackendResult>> { + self.ensure_connected()?; + // Note: We can't mutate stats here since we only have &self + // A real implementation might use interior mutability + Ok(self.store.get(id).cloned()) + } + + async fn delete(&mut self, id: T) -> BackendResult { + self.ensure_connected()?; + self.stats.deletes += 1; + Ok(self.store.delete(id)) + } + + async fn query(&self, pattern: &LinkPattern) -> BackendResult>> { + self.ensure_connected()?; + // Note: We can't mutate stats here since we only have &self + let results = self.store.find(pattern).into_iter().cloned().collect(); + Ok(results) + } + + fn capabilities(&self) -> BackendCapabilities { + BackendCapabilities { + supports_transactions: false, + supports_batch_operations: false, // We simulate batch ops + durability_level: DurabilityLevel::None, + max_link_size: 0, // Unlimited + supports_pattern_queries: true, + } + } + + fn stats(&self) -> BackendStats { + let now = Self::current_time_ms(); + let uptime = self.connected_at.map_or(0, |at| now - at); + + BackendStats { + total_links: self.store.total_count(), + used_space: 0, // Not tracked for memory backend + operations: self.stats.clone(), + connected_at: self.connected_at, + uptime_ms: uptime, + } + } +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + use crate::LinkRef; + + #[tokio::test] + async fn test_memory_backend_connect_disconnect() { + let mut backend = MemoryBackend::::new(); + assert!(!backend.is_connected()); + + backend.connect().await.unwrap(); + assert!(backend.is_connected()); + + backend.disconnect().await.unwrap(); + assert!(!backend.is_connected()); + } + + #[tokio::test] + async fn test_memory_backend_save_load() { + let mut backend = MemoryBackend::::new(); + backend.connect().await.unwrap(); + + let link = Link::new(0, LinkRef::Id(1), LinkRef::Id(2)); + let id = backend.save(link).await.unwrap(); + assert_ne!(id, 0); + + let loaded = backend.load(id).await.unwrap(); + assert!(loaded.is_some()); + let loaded = loaded.unwrap(); + assert_eq!(loaded.id, id); + assert_eq!(loaded.source_id(), 1); + assert_eq!(loaded.target_id(), 2); + } + + #[tokio::test] + async fn test_memory_backend_delete() { + let mut backend = MemoryBackend::::new(); + backend.connect().await.unwrap(); + + let link = Link::new(0, LinkRef::Id(1), LinkRef::Id(2)); + let id = backend.save(link).await.unwrap(); + + assert!(backend.delete(id).await.unwrap()); + assert!(!backend.delete(id).await.unwrap()); // Already deleted + } + + #[tokio::test] + async fn test_memory_backend_query() { + let mut backend = MemoryBackend::::new(); + backend.connect().await.unwrap(); + + backend + .save(Link::new(0, LinkRef::Id(1), LinkRef::Id(2))) + .await + .unwrap(); + backend + .save(Link::new(0, LinkRef::Id(1), LinkRef::Id(3))) + .await + .unwrap(); + backend + .save(Link::new(0, LinkRef::Id(2), LinkRef::Id(3))) + .await + .unwrap(); + + let results = backend + .query(&LinkPattern::with_source(LinkRef::Id(1))) + .await + .unwrap(); + assert_eq!(results.len(), 2); + } + + #[tokio::test] + async fn test_memory_backend_not_connected_error() { + let mut backend = MemoryBackend::::new(); + // Don't connect + + let result = backend + .save(Link::new(0, LinkRef::Id(1), LinkRef::Id(2))) + .await; + assert!(matches!(result, Err(BackendError::NotConnected))); + } + + #[tokio::test] + async fn test_memory_backend_save_batch() { + let mut backend = MemoryBackend::::new(); + backend.connect().await.unwrap(); + + let links = vec![ + Link::new(0, LinkRef::Id(1), LinkRef::Id(2)), + Link::new(0, LinkRef::Id(3), LinkRef::Id(4)), + ]; + + let ids = backend.save_batch(links).await.unwrap(); + assert_eq!(ids.len(), 2); + assert_ne!(ids[0], ids[1]); + } + + #[tokio::test] + async fn test_memory_backend_delete_batch() { + let mut backend = MemoryBackend::::new(); + backend.connect().await.unwrap(); + + let id1 = backend + .save(Link::new(0, LinkRef::Id(1), LinkRef::Id(2))) + .await + .unwrap(); + let id2 = backend + .save(Link::new(0, LinkRef::Id(3), LinkRef::Id(4))) + .await + .unwrap(); + + let results = backend.delete_batch(vec![id1, id2, 999]).await.unwrap(); + assert_eq!(results, vec![true, true, false]); + } + + #[test] + fn test_memory_backend_capabilities() { + let backend = MemoryBackend::::new(); + let caps = backend.capabilities(); + + assert!(!caps.supports_transactions); + assert!(!caps.supports_batch_operations); + assert_eq!(caps.durability_level, DurabilityLevel::None); + assert_eq!(caps.max_link_size, 0); + assert!(caps.supports_pattern_queries); + } + + #[tokio::test] + async fn test_memory_backend_stats() { + let mut backend = MemoryBackend::::new(); + backend.connect().await.unwrap(); + + backend + .save(Link::new(0, LinkRef::Id(1), LinkRef::Id(2))) + .await + .unwrap(); + backend + .save(Link::new(0, LinkRef::Id(3), LinkRef::Id(4))) + .await + .unwrap(); + + let stats = backend.stats(); + assert_eq!(stats.total_links, 2); + assert_eq!(stats.operations.writes, 2); + assert!(stats.connected_at.is_some()); + // uptime_ms is u64, always non-negative - just verify it's tracked + assert!(stats.uptime_ms > 0 || stats.connected_at.is_some()); + } +} diff --git a/rust/src/backends/mod.rs b/rust/src/backends/mod.rs index 6071f57..61148bf 100644 --- a/rust/src/backends/mod.rs +++ b/rust/src/backends/mod.rs @@ -1,30 +1,94 @@ //! Storage backend implementations for links-queue. //! -//! This module provides various storage backends that implement the [`LinkStore`] trait. +//! This module provides various storage backends that implement the [`StorageBackend`] trait. //! Each backend offers different tradeoffs between performance, durability, and features. //! //! # Available Backends //! -//! - [`MemoryLinkStore`] - In-memory storage with O(1) lookups, ideal for testing and development. +//! - [`MemoryLinkStore`] - Low-level in-memory link store implementing [`LinkStore`] +//! - [`MemoryBackend`] - In-memory storage backend implementing [`StorageBackend`] //! -//! # Example +//! # Backend Selection +//! +//! Use [`BackendRegistry`] to create backends from configuration: +//! +//! ```rust +//! use links_queue::{BackendRegistry, BackendConfig}; +//! +//! let registry = BackendRegistry::::new(); +//! +//! // Create memory backend (default) +//! let backend = registry.create(&BackendConfig::memory()).unwrap(); +//! +//! // Create memory backend with capacity hint +//! let backend = registry.create(&BackendConfig::memory_with_capacity(10000)).unwrap(); +//! ``` +//! +//! # Using `StorageBackend` Directly //! //! ```rust -//! use links_queue::{MemoryLinkStore, LinkStore, LinkRef, LinkPattern}; +//! use links_queue::{MemoryBackend, StorageBackend, Link, LinkRef, LinkPattern}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! let mut backend = MemoryBackend::::new(); +//! backend.connect().await?; +//! +//! // Save a link +//! let link = Link::new(0, LinkRef::Id(2), LinkRef::Id(3)); +//! let id = backend.save(link).await?; +//! println!("Created link with ID: {}", id); //! -//! let mut store = MemoryLinkStore::::new(); +//! // Query links +//! let results = backend.query(&LinkPattern::with_source(LinkRef::Id(2))).await?; +//! println!("Found {} matching links", results.len()); //! -//! // Create a link -//! let link = store.create(LinkRef::Id(2), LinkRef::Id(3)).unwrap(); -//! println!("Created link: {:?}", link); +//! backend.disconnect().await?; +//! Ok(()) +//! } +//! ``` +//! +//! # Custom Backends +//! +//! Implement the [`StorageBackend`] trait for custom storage: +//! +//! ```rust,ignore +//! use links_queue::{StorageBackend, Link, LinkPattern, LinkType, BackendResult}; +//! +//! struct MyBackend { +//! // ... fields +//! } //! -//! // Find links by pattern -//! let results = store.find(&LinkPattern::with_source(LinkRef::Id(2))); -//! assert_eq!(results.len(), 1); +//! impl StorageBackend for MyBackend { +//! // ... implement required methods +//! } +//! +//! // Register with the registry +//! let mut registry = BackendRegistry::::new(); +//! registry.register("my-backend", Arc::new(|config| { +//! Ok(Box::new(MyBackend::new()) as _) +//! })); //! ``` mod memory; +mod memory_backend; +mod registry; +mod traits; + #[cfg(test)] mod memory_tests; +// Re-export low-level link store pub use memory::MemoryLinkStore; + +// Re-export storage backend trait and types +pub use traits::{ + BackendCapabilities, BackendError, BackendResult, BackendStats, DurabilityLevel, + OperationStats, StorageBackend, +}; + +// Re-export memory backend +pub use memory_backend::MemoryBackend; + +// Re-export registry +pub use registry::{BackendConfig, BackendRegistry, StorageBackendDyn}; diff --git a/rust/src/backends/registry.rs b/rust/src/backends/registry.rs new file mode 100644 index 0000000..96932a1 --- /dev/null +++ b/rust/src/backends/registry.rs @@ -0,0 +1,582 @@ +//! Backend registry for links-queue. +//! +//! This module provides a registry for storage backend implementations, +//! allowing backends to be registered and instantiated by name via configuration. +//! +//! # Design +//! +//! The registry uses type-erased factory functions to allow dynamic backend +//! creation without requiring trait objects for the backends themselves. +//! +//! # Example +//! +//! ```rust +//! use links_queue::{BackendRegistry, MemoryBackend, BackendConfig}; +//! +//! // Create a registry +//! let mut registry = BackendRegistry::::new(); +//! +//! // Built-in "memory" backend is pre-registered +//! assert!(registry.has("memory")); +//! +//! // Create a backend from config +//! let backend = registry.create(&BackendConfig::memory()).unwrap(); +//! ``` + +use std::collections::HashMap; +use std::sync::Arc; + +use crate::LinkType; + +use super::memory_backend::MemoryBackend; +use super::traits::{BackendError, BackendResult, StorageBackend}; + +// ============================================================================= +// Backend Configuration +// ============================================================================= + +/// Configuration for backend creation. +/// +/// This struct holds the backend type name and any type-specific options +/// needed to create a backend instance. +/// +/// # Example +/// +/// ```rust +/// use links_queue::BackendConfig; +/// +/// // Memory backend (default) +/// let config = BackendConfig::memory(); +/// +/// // Memory backend with capacity hint +/// let config = BackendConfig::memory_with_capacity(10000); +/// +/// // Custom backend +/// let config = BackendConfig::new("my-custom") +/// .with_option("host", "localhost") +/// .with_option("port", "6379"); +/// ``` +#[derive(Debug, Clone)] +pub struct BackendConfig { + /// The backend type name. + pub backend_type: String, + + /// Type-specific options as key-value pairs. + pub options: HashMap, +} + +impl BackendConfig { + /// Creates a new backend configuration. + /// + /// # Arguments + /// + /// * `backend_type` - The name of the backend type + /// + /// # Example + /// + /// ```rust + /// use links_queue::BackendConfig; + /// + /// let config = BackendConfig::new("redis"); + /// ``` + #[must_use] + pub fn new(backend_type: impl Into) -> Self { + Self { + backend_type: backend_type.into(), + options: HashMap::new(), + } + } + + /// Creates a configuration for the memory backend. + /// + /// # Example + /// + /// ```rust + /// use links_queue::BackendConfig; + /// + /// let config = BackendConfig::memory(); + /// ``` + #[must_use] + pub fn memory() -> Self { + Self::new("memory") + } + + /// Creates a configuration for the memory backend with a capacity hint. + /// + /// # Arguments + /// + /// * `capacity` - Initial capacity for the link storage + /// + /// # Example + /// + /// ```rust + /// use links_queue::BackendConfig; + /// + /// let config = BackendConfig::memory_with_capacity(10000); + /// ``` + #[must_use] + pub fn memory_with_capacity(capacity: usize) -> Self { + Self::new("memory").with_option("capacity", capacity.to_string()) + } + + /// Adds an option to the configuration (builder pattern). + /// + /// # Arguments + /// + /// * `key` - Option name + /// * `value` - Option value + /// + /// # Example + /// + /// ```rust + /// use links_queue::BackendConfig; + /// + /// let config = BackendConfig::new("redis") + /// .with_option("host", "localhost") + /// .with_option("port", "6379"); + /// ``` + #[must_use] + pub fn with_option(mut self, key: impl Into, value: impl Into) -> Self { + self.options.insert(key.into(), value.into()); + self + } + + /// Gets an option value. + /// + /// # Arguments + /// + /// * `key` - Option name + /// + /// # Returns + /// + /// The option value if present. + #[must_use] + pub fn get_option(&self, key: &str) -> Option<&str> { + self.options.get(key).map(String::as_str) + } +} + +impl Default for BackendConfig { + fn default() -> Self { + Self::memory() + } +} + +// ============================================================================= +// Backend Factory +// ============================================================================= + +/// Type alias for backend factory functions. +/// +/// Factory functions create a boxed backend from a configuration. +type BackendFactory = + Arc BackendResult>> + Send + Sync>; + +/// Object-safe version of `StorageBackend` for use with registry. +/// +/// This trait allows storing backends as trait objects while maintaining +/// the async interface through boxed futures. +#[allow(clippy::type_complexity)] +pub trait StorageBackendDyn: Send + Sync { + /// Connects to the backend. + fn connect_dyn( + &mut self, + ) -> std::pin::Pin> + Send + '_>>; + + /// Disconnects from the backend. + fn disconnect_dyn( + &mut self, + ) -> std::pin::Pin> + Send + '_>>; + + /// Checks if connected. + fn is_connected_dyn(&self) -> bool; + + /// Saves a link. + fn save_dyn( + &mut self, + link: crate::Link, + ) -> std::pin::Pin> + Send + '_>>; + + /// Loads a link. + fn load_dyn( + &self, + id: T, + ) -> std::pin::Pin< + Box>>> + Send + '_>, + >; + + /// Deletes a link. + fn delete_dyn( + &mut self, + id: T, + ) -> std::pin::Pin> + Send + '_>>; + + /// Queries links. + fn query_dyn<'a>( + &'a self, + pattern: &'a crate::LinkPattern, + ) -> std::pin::Pin< + Box>>> + Send + 'a>, + >; + + /// Returns capabilities. + fn capabilities_dyn(&self) -> super::traits::BackendCapabilities; + + /// Returns stats. + fn stats_dyn(&self) -> super::traits::BackendStats; +} + +impl + 'static> StorageBackendDyn for B { + fn connect_dyn( + &mut self, + ) -> std::pin::Pin> + Send + '_>> + { + Box::pin(self.connect()) + } + + fn disconnect_dyn( + &mut self, + ) -> std::pin::Pin> + Send + '_>> + { + Box::pin(self.disconnect()) + } + + fn is_connected_dyn(&self) -> bool { + self.is_connected() + } + + fn save_dyn( + &mut self, + link: crate::Link, + ) -> std::pin::Pin> + Send + '_>> { + Box::pin(self.save(link)) + } + + fn load_dyn( + &self, + id: T, + ) -> std::pin::Pin< + Box>>> + Send + '_>, + > { + Box::pin(self.load(id)) + } + + fn delete_dyn( + &mut self, + id: T, + ) -> std::pin::Pin> + Send + '_>> + { + Box::pin(self.delete(id)) + } + + fn query_dyn<'a>( + &'a self, + pattern: &'a crate::LinkPattern, + ) -> std::pin::Pin< + Box>>> + Send + 'a>, + > { + Box::pin(self.query(pattern)) + } + + fn capabilities_dyn(&self) -> super::traits::BackendCapabilities { + self.capabilities() + } + + fn stats_dyn(&self) -> super::traits::BackendStats { + self.stats() + } +} + +// ============================================================================= +// Backend Registry +// ============================================================================= + +/// Registry for storage backend implementations. +/// +/// The registry allows backends to be registered by name and created +/// from configuration. Built-in backends (like `memory`) are pre-registered. +/// +/// # Type Parameters +/// +/// * `T` - The link ID type (must implement [`LinkType`]) +/// +/// # Example +/// +/// ```rust +/// use links_queue::{BackendRegistry, BackendConfig, MemoryBackend}; +/// +/// let mut registry = BackendRegistry::::new(); +/// +/// // Memory backend is pre-registered +/// let backend = registry.create(&BackendConfig::memory()).unwrap(); +/// +/// // List available backends +/// for name in registry.list_types() { +/// println!("Available: {}", name); +/// } +/// ``` +pub struct BackendRegistry { + /// Map of backend names to factory functions. + factories: HashMap>, +} + +impl Default for BackendRegistry { + fn default() -> Self { + Self::new() + } +} + +impl BackendRegistry { + /// Creates a new backend registry with built-in backends registered. + /// + /// # Example + /// + /// ```rust + /// use links_queue::BackendRegistry; + /// + /// let registry = BackendRegistry::::new(); + /// assert!(registry.has("memory")); + /// ``` + #[must_use] + pub fn new() -> Self { + let mut registry = Self { + factories: HashMap::new(), + }; + + // Register built-in memory backend + registry.register_memory_backend(); + + registry + } + + /// Registers the built-in memory backend. + fn register_memory_backend(&mut self) { + self.factories.insert( + "memory".to_string(), + Arc::new(|config: &BackendConfig| { + let capacity = config + .get_option("capacity") + .and_then(|s| s.parse::().ok()); + + let backend = + capacity.map_or_else(MemoryBackend::new, MemoryBackend::with_capacity); + + Ok(Box::new(backend) as Box>) + }), + ); + } + + /// Registers a custom backend factory. + /// + /// # Arguments + /// + /// * `name` - The name to register the backend under + /// * `factory` - A function that creates backend instances from config + /// + /// # Example + /// + /// ```rust,ignore + /// use links_queue::{BackendRegistry, BackendConfig, MemoryBackend}; + /// use std::sync::Arc; + /// + /// let mut registry = BackendRegistry::::new(); + /// + /// // Register a custom backend factory + /// registry.register("custom", Arc::new(|config: &BackendConfig| { + /// Ok(Box::new(MemoryBackend::::new()) as _) + /// })); + /// ``` + pub fn register(&mut self, name: impl Into, factory: Arc) + where + F: Fn(&BackendConfig) -> BackendResult>> + + Send + + Sync + + 'static, + { + self.factories.insert(name.into(), factory); + } + + /// Unregisters a backend. + /// + /// # Arguments + /// + /// * `name` - The name of the backend to unregister + /// + /// # Returns + /// + /// True if the backend was unregistered, false if it wasn't registered. + pub fn unregister(&mut self, name: &str) -> bool { + self.factories.remove(name).is_some() + } + + /// Checks if a backend is registered. + /// + /// # Arguments + /// + /// * `name` - The backend name to check + /// + /// # Returns + /// + /// True if registered. + #[must_use] + pub fn has(&self, name: &str) -> bool { + self.factories.contains_key(name) + } + + /// Creates a backend instance from configuration. + /// + /// # Arguments + /// + /// * `config` - Backend configuration + /// + /// # Returns + /// + /// A boxed backend instance. + /// + /// # Errors + /// + /// Returns an error if the backend type is not registered or creation fails. + /// + /// # Example + /// + /// ```rust + /// use links_queue::{BackendRegistry, BackendConfig}; + /// + /// let registry = BackendRegistry::::new(); + /// let backend = registry.create(&BackendConfig::memory()).unwrap(); + /// ``` + pub fn create( + &self, + config: &BackendConfig, + ) -> BackendResult>> { + let factory = self.factories.get(&config.backend_type).ok_or_else(|| { + BackendError::Other(format!( + "Unknown backend type: \"{}\". Available types: {}", + config.backend_type, + self.list_types().join(", ") + )) + })?; + + factory(config) + } + + /// Lists all registered backend types. + /// + /// # Returns + /// + /// Vector of registered backend type names. + #[must_use] + pub fn list_types(&self) -> Vec { + self.factories.keys().cloned().collect() + } + + /// Resets the registry to default state (only built-in backends). + pub fn reset(&mut self) { + self.factories.clear(); + self.register_memory_backend(); + } +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_backend_config_new() { + let config = BackendConfig::new("test"); + assert_eq!(config.backend_type, "test"); + assert!(config.options.is_empty()); + } + + #[test] + fn test_backend_config_memory() { + let config = BackendConfig::memory(); + assert_eq!(config.backend_type, "memory"); + } + + #[test] + fn test_backend_config_with_capacity() { + let config = BackendConfig::memory_with_capacity(1000); + assert_eq!(config.backend_type, "memory"); + assert_eq!(config.get_option("capacity"), Some("1000")); + } + + #[test] + fn test_backend_config_with_option() { + let config = BackendConfig::new("redis") + .with_option("host", "localhost") + .with_option("port", "6379"); + + assert_eq!(config.get_option("host"), Some("localhost")); + assert_eq!(config.get_option("port"), Some("6379")); + assert_eq!(config.get_option("nonexistent"), None); + } + + #[test] + fn test_registry_new() { + let registry = BackendRegistry::::new(); + assert!(registry.has("memory")); + } + + #[test] + fn test_registry_create_memory() { + let registry = BackendRegistry::::new(); + let result = registry.create(&BackendConfig::memory()); + assert!(result.is_ok()); + } + + #[test] + fn test_registry_create_unknown() { + let registry = BackendRegistry::::new(); + let result = registry.create(&BackendConfig::new("nonexistent")); + assert!(result.is_err()); + } + + #[test] + fn test_registry_list_types() { + let registry = BackendRegistry::::new(); + let types = registry.list_types(); + assert!(types.contains(&"memory".to_string())); + } + + #[test] + fn test_registry_unregister() { + let mut registry = BackendRegistry::::new(); + assert!(registry.has("memory")); + assert!(registry.unregister("memory")); + assert!(!registry.has("memory")); + assert!(!registry.unregister("memory")); // Already unregistered + } + + #[test] + fn test_registry_reset() { + let mut registry = BackendRegistry::::new(); + registry.unregister("memory"); + assert!(!registry.has("memory")); + + registry.reset(); + assert!(registry.has("memory")); + } + + #[tokio::test] + async fn test_created_backend_works() { + let registry = BackendRegistry::::new(); + let mut backend = registry.create(&BackendConfig::memory()).unwrap(); + + backend.connect_dyn().await.unwrap(); + assert!(backend.is_connected_dyn()); + + let link = crate::Link::new(0, crate::LinkRef::Id(1), crate::LinkRef::Id(2)); + let id = backend.save_dyn(link).await.unwrap(); + assert_ne!(id, 0); + + let loaded = backend.load_dyn(id).await.unwrap(); + assert!(loaded.is_some()); + + backend.disconnect_dyn().await.unwrap(); + } +} diff --git a/rust/src/backends/traits.rs b/rust/src/backends/traits.rs new file mode 100644 index 0000000..b9b7538 --- /dev/null +++ b/rust/src/backends/traits.rs @@ -0,0 +1,501 @@ +//! Storage backend trait definitions for links-queue. +//! +//! This module defines the [`StorageBackend`] trait that enables pluggable +//! storage backends. Implementations can provide different tradeoffs between +//! performance, durability, and features. +//! +//! # Design Goals +//! +//! - **Async-first**: All operations are async for compatibility with various backends +//! - **Pluggable**: Backends can be swapped via configuration without code changes +//! - **Observable**: Backends expose capabilities and statistics for monitoring +//! +//! # Example +//! +//! ```rust +//! use links_queue::{ +//! StorageBackend, MemoryBackend, Link, LinkRef, LinkPattern, LinkType, +//! BackendCapabilities, BackendStats, DurabilityLevel, +//! }; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Create and connect to backend +//! let mut backend = MemoryBackend::::new(); +//! backend.connect().await?; +//! +//! // Save a link +//! let link = Link::new(0u64, LinkRef::Id(1), LinkRef::Id(2)); +//! let id = backend.save(link).await?; +//! +//! // Load it back +//! if let Some(loaded) = backend.load(id).await? { +//! println!("Loaded: {:?}", loaded); +//! } +//! +//! // Query by pattern +//! let links = backend.query(&LinkPattern::with_source(LinkRef::Id(1))).await?; +//! println!("Found {} links", links.len()); +//! +//! // Disconnect +//! backend.disconnect().await?; +//! Ok(()) +//! } +//! ``` +//! +//! # See Also +//! +//! - [`MemoryBackend`](super::MemoryBackend) - In-memory storage backend implementation +//! - [`LinkStore`](crate::LinkStore) - Lower-level link store trait + +use std::fmt::Debug; + +use crate::{Link, LinkError, LinkPattern, LinkType}; + +// ============================================================================= +// Durability Level +// ============================================================================= + +/// Durability level supported by a storage backend. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum DurabilityLevel { + /// No durability guarantees (in-memory only). + #[default] + None, + /// Data is synced to disk. + Fsync, + /// Data is replicated across multiple nodes. + Replicated, +} + +// ============================================================================= +// Backend Capabilities +// ============================================================================= + +/// Describes the capabilities of a storage backend. +/// +/// This allows the system to validate that a backend meets the requirements +/// for a given operating mode and to adapt behavior accordingly. +/// +/// # Example +/// +/// ```rust +/// use links_queue::{BackendCapabilities, DurabilityLevel}; +/// +/// let caps = BackendCapabilities { +/// supports_transactions: false, +/// supports_batch_operations: true, +/// durability_level: DurabilityLevel::None, +/// max_link_size: 0, +/// supports_pattern_queries: true, +/// }; +/// +/// if caps.durability_level == DurabilityLevel::None { +/// println!("Warning: Data will be lost on restart"); +/// } +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BackendCapabilities { + /// Whether the backend supports atomic transactions. + pub supports_transactions: bool, + + /// Whether the backend supports batch operations natively. + /// If false, batch operations are simulated with individual operations. + pub supports_batch_operations: bool, + + /// The durability level provided by this backend. + pub durability_level: DurabilityLevel, + + /// Maximum size of a single link in bytes (0 for unlimited). + pub max_link_size: usize, + + /// Whether the backend supports pattern-based queries natively. + /// If false, queries are performed by scanning all links. + pub supports_pattern_queries: bool, +} + +impl Default for BackendCapabilities { + fn default() -> Self { + Self { + supports_transactions: false, + supports_batch_operations: false, + durability_level: DurabilityLevel::None, + max_link_size: 0, + supports_pattern_queries: true, + } + } +} + +// ============================================================================= +// Operation Statistics +// ============================================================================= + +/// Operation counters for backend statistics. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct OperationStats { + /// Number of read operations performed. + pub reads: u64, + + /// Number of write operations performed (save/update). + pub writes: u64, + + /// Number of delete operations performed. + pub deletes: u64, + + /// Number of query operations performed. + pub queries: u64, +} + +// ============================================================================= +// Backend Statistics +// ============================================================================= + +/// Statistics and metrics for a storage backend. +/// +/// # Example +/// +/// ```rust +/// use links_queue::BackendStats; +/// +/// fn print_stats(stats: &BackendStats) { +/// println!("Total links: {}", stats.total_links); +/// println!("Used space: {} bytes", stats.used_space); +/// println!("Reads: {}", stats.operations.reads); +/// println!("Writes: {}", stats.operations.writes); +/// } +/// ``` +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct BackendStats { + /// Total number of links stored. + pub total_links: usize, + + /// Approximate storage space used in bytes. + pub used_space: usize, + + /// Operation counters. + pub operations: OperationStats, + + /// Timestamp when the backend was connected (Unix timestamp in milliseconds). + /// None if not connected. + pub connected_at: Option, + + /// Uptime in milliseconds since connection. + pub uptime_ms: u64, +} + +// ============================================================================= +// Backend Error +// ============================================================================= + +/// Errors that can occur during backend operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum BackendError { + /// The backend is not connected. + NotConnected, + + /// Connection to the backend failed. + ConnectionFailed(String), + + /// A link operation failed. + LinkError(LinkError), + + /// A generic error occurred. + Other(String), +} + +impl std::fmt::Display for BackendError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotConnected => write!(f, "Backend is not connected"), + Self::ConnectionFailed(msg) => write!(f, "Connection failed: {msg}"), + Self::LinkError(err) => write!(f, "Link error: {err}"), + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl std::error::Error for BackendError {} + +impl From> for BackendError { + fn from(err: LinkError) -> Self { + Self::LinkError(err) + } +} + +/// Result type for backend operations. +pub type BackendResult = Result>; + +// ============================================================================= +// Storage Backend Trait +// ============================================================================= + +/// Trait for pluggable storage backends. +/// +/// This abstraction allows switching between memory, link-cli, and custom +/// backends via configuration without code changes. +/// +/// # Type Parameters +/// +/// * `T` - The link ID type (must implement [`LinkType`]) +/// +/// # Implementation Notes +/// +/// - All methods are async for compatibility with various backend types +/// - Implementors should track connection state and reject operations when disconnected +/// - Batch operations have default implementations but can be overridden for efficiency +/// +/// # Example Implementation +/// +/// ```rust,ignore +/// use links_queue::{StorageBackend, Link, LinkRef, LinkPattern, LinkType, BackendResult}; +/// +/// struct MyBackend { +/// connected: bool, +/// // ... other fields +/// } +/// +/// #[async_trait::async_trait] +/// impl StorageBackend for MyBackend { +/// async fn connect(&mut self) -> BackendResult { +/// self.connected = true; +/// Ok(()) +/// } +/// +/// async fn disconnect(&mut self) -> BackendResult { +/// self.connected = false; +/// Ok(()) +/// } +/// +/// fn is_connected(&self) -> bool { +/// self.connected +/// } +/// +/// // ... implement other methods +/// } +/// ``` +pub trait StorageBackend: Send + Sync { + // ------------------------------------------------------------------------- + // Lifecycle Operations + // ------------------------------------------------------------------------- + + /// Establishes a connection to the storage backend. + /// + /// This should be called before any other operations. For in-memory backends, + /// this may be a no-op but should still be called for consistency. + /// + /// # Errors + /// + /// Returns [`BackendError::ConnectionFailed`] if connection fails. + fn connect(&mut self) -> impl std::future::Future> + Send; + + /// Gracefully disconnects from the storage backend. + /// + /// This should flush any pending writes and release resources. + fn disconnect(&mut self) -> impl std::future::Future> + Send; + + /// Checks if the backend is currently connected. + /// + /// # Returns + /// + /// True if connected and ready for operations. + fn is_connected(&self) -> bool; + + // ------------------------------------------------------------------------- + // Core Operations + // ------------------------------------------------------------------------- + + /// Saves a link to storage. + /// + /// If the link's ID is zero (nothing), a new ID will be assigned. + /// If a link with the same structure exists (deduplication), the existing + /// ID may be returned depending on the backend's deduplication strategy. + /// + /// # Arguments + /// + /// * `link` - The link to save + /// + /// # Returns + /// + /// The ID of the saved link (may be new or existing). + /// + /// # Errors + /// + /// Returns [`BackendError::NotConnected`] if not connected. + fn save( + &mut self, + link: Link, + ) -> impl std::future::Future> + Send; + + /// Loads a link by its ID. + /// + /// # Arguments + /// + /// * `id` - The ID of the link to load + /// + /// # Returns + /// + /// The link if found, None otherwise. + /// + /// # Errors + /// + /// Returns [`BackendError::NotConnected`] if not connected. + fn load( + &self, + id: T, + ) -> impl std::future::Future>>> + Send; + + /// Deletes a link by its ID. + /// + /// # Arguments + /// + /// * `id` - The ID of the link to delete + /// + /// # Returns + /// + /// True if the link was deleted, false if it didn't exist. + /// + /// # Errors + /// + /// Returns [`BackendError::NotConnected`] if not connected. + fn delete(&mut self, id: T) + -> impl std::future::Future> + Send; + + /// Queries links matching a pattern. + /// + /// # Arguments + /// + /// * `pattern` - The pattern to match + /// + /// # Returns + /// + /// Vector of matching links. + /// + /// # Errors + /// + /// Returns [`BackendError::NotConnected`] if not connected. + fn query( + &self, + pattern: &LinkPattern, + ) -> impl std::future::Future>>> + Send; + + // ------------------------------------------------------------------------- + // Batch Operations (default implementations) + // ------------------------------------------------------------------------- + + /// Saves multiple links in a batch. + /// + /// For backends that support native batch operations, this can be overridden + /// for better efficiency. The default implementation calls `save()` for each link. + /// + /// # Arguments + /// + /// * `links` - Vector of links to save + /// + /// # Returns + /// + /// Vector of IDs (in the same order as input). + fn save_batch( + &mut self, + links: Vec>, + ) -> impl std::future::Future>> + Send { + async move { + let mut ids = Vec::with_capacity(links.len()); + for link in links { + ids.push(self.save(link).await?); + } + Ok(ids) + } + } + + /// Deletes multiple links by their IDs. + /// + /// The default implementation calls `delete()` for each ID. + /// + /// # Arguments + /// + /// * `ids` - Vector of IDs to delete + /// + /// # Returns + /// + /// Vector of booleans indicating success for each deletion (in order). + fn delete_batch( + &mut self, + ids: Vec, + ) -> impl std::future::Future>> + Send { + async move { + let mut results = Vec::with_capacity(ids.len()); + for id in ids { + results.push(self.delete(id).await?); + } + Ok(results) + } + } + + // ------------------------------------------------------------------------- + // Metadata Operations + // ------------------------------------------------------------------------- + + /// Returns the capabilities of this backend. + fn capabilities(&self) -> BackendCapabilities; + + /// Returns statistics about the backend. + fn stats(&self) -> BackendStats; +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_durability_level_default() { + assert_eq!(DurabilityLevel::default(), DurabilityLevel::None); + } + + #[test] + fn test_backend_capabilities_default() { + let caps = BackendCapabilities::default(); + assert!(!caps.supports_transactions); + assert!(!caps.supports_batch_operations); + assert_eq!(caps.durability_level, DurabilityLevel::None); + assert_eq!(caps.max_link_size, 0); + assert!(caps.supports_pattern_queries); + } + + #[test] + fn test_operation_stats_default() { + let stats = OperationStats::default(); + assert_eq!(stats.reads, 0); + assert_eq!(stats.writes, 0); + assert_eq!(stats.deletes, 0); + assert_eq!(stats.queries, 0); + } + + #[test] + fn test_backend_stats_default() { + let stats = BackendStats::default(); + assert_eq!(stats.total_links, 0); + assert_eq!(stats.used_space, 0); + assert!(stats.connected_at.is_none()); + assert_eq!(stats.uptime_ms, 0); + } + + #[test] + fn test_backend_error_display() { + let err: BackendError = BackendError::NotConnected; + assert!(err.to_string().contains("not connected")); + + let err2: BackendError = BackendError::ConnectionFailed("timeout".to_string()); + assert!(err2.to_string().contains("timeout")); + } + + #[test] + fn test_backend_error_from_link_error() { + let link_err = LinkError::NotFound(42u64); + let backend_err: BackendError = link_err.into(); + assert!(matches!(backend_err, BackendError::LinkError(_))); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index d8f43f5..6c02a03 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -79,6 +79,12 @@ pub use traits::{ Any, Link, LinkError, LinkPattern, LinkRef, LinkResult, LinkStore, LinkType, PatternField, }; +// Re-export storage backend types +pub use backends::{ + BackendCapabilities, BackendConfig, BackendError, BackendRegistry, BackendResult, BackendStats, + DurabilityLevel, MemoryBackend, OperationStats, StorageBackend, StorageBackendDyn, +}; + // ============================================================================= // Package Version // =============================================================================