Files
smartfs/readme.hints.md

394 lines
10 KiB
Markdown
Raw Normal View History

2025-11-21 18:36:31 +00:00
# SmartFS Architecture Hints
## Overview
SmartFS is a modern, pluggable filesystem module built with TypeScript. It provides a fluent API for filesystem operations with support for multiple storage backends, transactions, streaming, and file watching.
## Core Design Principles
### 1. Fluent API with Action-Last Pattern
The API uses a **builder pattern** where configuration methods return `this` for chaining, and action methods return `Promise` for execution:
```typescript
await fs.file('/path')
.encoding('utf8') // configuration
.atomic() // configuration
.write('content'); // action (returns Promise)
```
**Reasoning:**
- Configuration is explicit and discoverable
- Action methods clearly indicate execution points
- Natural reading order: "configure what you want, then do it"
- Type-safe chaining prevents invalid operations
### 2. Provider Architecture
All filesystem operations go through a provider interface (`ISmartFsProvider`). This allows:
- **Pluggable backends**: Node.js fs, memory, S3, etc.
- **Testing**: Use memory provider for fast, isolated tests
- **Abstraction**: Hide platform-specific details
- **Extensibility**: Easy to add new storage backends
**Provider Responsibilities:**
- Implement all filesystem operations
- Handle path normalization
- Provide capability flags
- Implement transactions (or fall back to sequential)
### 3. Async-Only Design
No synchronous operations are exposed. All methods return Promises.
**Reasoning:**
- Modern async/await patterns
- Better performance (non-blocking)
- Consistent API surface
- Simplifies implementation
### 4. Web Streams API
Uses Web Streams (`ReadableStream`, `WritableStream`) instead of Node.js streams.
**Reasoning:**
- Standard API across platforms (Node.js, browser, Deno)
- Better composability with `.pipeTo()`
- Backpressure handling built-in
- Future-proof (web standard)
### 5. Transaction System
Transactions provide atomic multi-file operations with automatic rollback.
**Implementation:**
1. `prepareTransaction()` - Create backups of existing files
2. `executeTransaction()` - Execute all operations
3. `rollbackTransaction()` - Restore from backups if any operation fails
**Trade-offs:**
- Not truly atomic at OS level (no fsync barriers)
- Best-effort rollback (can fail if disk full, etc.)
- Sufficient for most use cases
### 6. File Watching
Event-based file watching with debouncing and filtering.
**Features:**
- Recursive watching
- Pattern filtering (glob, RegExp, function)
- Debouncing to reduce event spam
- Multiple event handlers per watcher
**Implementation Notes:**
- Node provider uses `fs.watch`
- Memory provider triggers events synchronously
- Events include stats when available
## Directory Structure
```
ts/
├── classes/
│ ├── smartfs.ts # Main entry point
│ ├── smartfs.file.ts # File builder
│ ├── smartfs.directory.ts # Directory builder
│ ├── smartfs.transaction.ts # Transaction builder
│ └── smartfs.watcher.ts # Watcher builder
├── interfaces/
│ ├── mod.provider.ts # Provider interface
│ └── mod.types.ts # Type definitions
├── providers/
│ ├── smartfs.provider.node.ts # Node.js implementation
│ └── smartfs.provider.memory.ts # Memory implementation
└── index.ts # Public exports
```
## Key Architectural Decisions
### Why Action-Last?
**Considered alternatives:**
- Action-first: `fs.file('/path').read().asText()` - Less intuitive
- Mixed: `fs.read('/path', { encoding: 'utf8' })` - Less fluent
**Chosen:** Action-last pattern
- Clear execution point
- Natural configuration flow
- Better IDE autocomplete
### Why Separate Builders?
Each builder (`SmartFsFile`, `SmartFsDirectory`, etc.) is a separate class.
**Benefits:**
- Type safety (can't call `.list()` on a file)
- Clear separation of concerns
- Better code organization
- Easier to extend
### Transaction Implementation
**Design choice:** Prepare → Execute → Rollback pattern
**Alternative considered:** Copy-on-write filesystem
- Would require provider-specific implementations
- More complex
- Not worth the complexity for most use cases
**Chosen approach:**
1. Read existing files before operations
2. Store backup data in operation objects
3. Rollback by restoring from backups
**Limitations:**
- Not truly atomic (no cross-file locks)
- Rollback can fail (rare)
- Memory overhead for large files
**Future improvements:**
- Providers could override with native transactions
- Add transaction isolation levels
- Support for distributed transactions
### Path Handling
All paths are normalized by the provider.
**Reasoning:**
- Providers know their path conventions
- Allows Windows vs. Unix path handling
- S3 provider can handle virtual paths
- Memory provider uses consistent format
### Error Handling
Errors bubble up from providers.
**Design:**
- No custom error wrapping
- Provider errors are descriptive
- Use standard Node.js error codes (ENOENT, etc.)
**Reasoning:**
- Simpler implementation
- Familiar error messages
- Less abstraction overhead
## Performance Considerations
### Streaming
Use streams for files > 1MB to avoid loading entire file into memory.
**Chunk size defaults:**
- Read: 64KB (configurable via `.chunkSize()`)
- Write: 16KB (Node.js default)
### Memory Provider
All data stored in Map<string, IMemoryEntry>.
**Trade-offs:**
- Fast (no I/O)
- Limited by available memory
- No persistence
- Perfect for testing
### Node Provider
Direct use of Node.js `fs/promises` API.
**Optimizations:**
- Atomic writes use temp files + rename (atomic at OS level)
- Move operations try rename first, fallback to copy+delete
- Stat caching not implemented (can add if needed)
## Testing Strategy
### Test Organization
- `test/test.memory.provider.ts` - Memory provider tests
- `test/test.node.provider.ts` - Node.js provider tests
- `test/test.ts` - Main test entry
### Testing Approach
1. **Memory provider tests** - Fast, no I/O, comprehensive
2. **Node provider tests** - Real filesystem, integration
3. **Both test suites** share similar test cases
**Reasoning:**
- Memory tests are fast and isolated
- Node tests verify real filesystem behavior
- Easy to add new provider tests
## Future Enhancements
### Potential Features
1. **S3 Provider** - Cloud storage backend
2. **FTP Provider** - Remote filesystem access
3. **Virtual Provider** - Union of multiple providers
4. **Caching Layer** - In-memory cache for frequently accessed files
5. **Compression** - Transparent compression/decompression
6. **Encryption** - Transparent encryption at rest
7. **Versioning** - Automatic file versioning
### API Extensions
1. **Shortcut methods** - `fs.read('/path')` as alternative to `fs.file('/path').read()`
2. **Batch operations** - `fs.batch().file(...).file(...).execute()`
3. **Query API** - SQL-like queries for file listings
4. **Hooks** - Before/after operation hooks
## Dependencies
### Core Dependencies
- `@push.rocks/smartpath` - Path utilities
- `@types/node` - TypeScript types for Node.js
### Why Minimal Dependencies?
**Philosophy:**
- Keep the core light
- Avoid dependency hell
- Easier to maintain
- Faster installation
**Trade-off:**
- More code to maintain
- Can't leverage external libraries
**Decision:**
- Worth it for a foundational library
- Providers can have their own dependencies
## Compatibility
### Node.js Versions
Requires Node.js 18+ for:
- Native Web Streams API
- `fs.rm()` (replaces deprecated `fs.rmdir()`)
- `fs/promises` API
### Browser Compatibility
Core architecture supports browser, but:
- No browser provider implemented yet
- Would need IndexedDB or similar backend
- Stream handling already compatible
### TypeScript
Uses ES2022 target, NodeNext modules.
**Reasoning:**
- Modern JavaScript features
- ESM-first approach
- Better tree-shaking
- Future-proof
## Common Patterns
### Provider Implementation Checklist
When implementing a new provider:
1. ✅ Implement `ISmartFsProvider` interface
2. ✅ Set capability flags correctly
3. ✅ Normalize paths in constructor/methods
4. ✅ Handle errors with descriptive messages
5. ✅ Implement transaction support (or fallback)
6. ✅ Add comprehensive tests
7. ✅ Document provider-specific limitations
### Builder Pattern Example
```typescript
class SmartFsFile {
private options = {};
// Configuration (returns this)
encoding(enc: string): this {
this.options.encoding = enc;
return this;
}
// Action (returns Promise)
async read(): Promise<string | Buffer> {
return this.provider.readFile(this.path, this.options);
}
}
```
## Known Limitations
### Transaction Atomicity
- Not truly atomic across files
- Rollback can fail in edge cases
- No distributed transaction support
**Mitigation:**
- Document limitations clearly
- Best-effort rollback is sufficient for most cases
- Providers can override with native transactions
### File Watching
- Node.js `fs.watch` has platform-specific behavior
- May miss rapid changes
- No guarantee of event order
**Mitigation:**
- Debouncing helps with rapid changes
- Document platform differences
- Memory provider has predictable behavior (testing)
### Path Handling
- No cross-provider path compatibility
- Provider-specific path formats
**Mitigation:**
- Document path format per provider
- Use provider's `normalizePath()` method
- Consider adding path conversion utilities
## Maintenance Notes
### When to Update
- **Breaking changes:** Avoid unless absolutely necessary
- **New features:** Add as fluent methods or new builders
- **Bug fixes:** Prioritize data integrity
- **Performance:** Profile before optimizing
### Code Style
- Use TypeScript strict mode
- Prefer composition over inheritance
- Keep classes focused (SRP)
- Document public APIs with JSDoc
- Use meaningful variable names
### Testing Requirements
All changes must:
- Pass existing tests
- Add new tests for new features
- Maintain >90% code coverage
- Test both memory and Node providers
## Resources
- [Web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
- [Node.js fs/promises](https://nodejs.org/api/fs.html#promises-api)
- [Builder Pattern](https://refactoring.guru/design-patterns/builder)
- [Provider Pattern](https://en.wikipedia.org/wiki/Provider_model)