diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..ac4c523 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,367 @@ +# GitHub Copilot Instructions for PolyBus + +## Project Overview + +PolyBus is a **polyglot messaging framework** that enables seamless communication between applications written in different programming languages. It provides unified interfaces across TypeScript, Python, and .NET (with PHP planned) for building interoperable distributed systems and microservices. + +## Core Architecture Principles + +### Layered Architecture +``` +Application Code → IPolyBus API → Handler Pipeline → Transaction Management → Transport Interface → Transport Implementations +``` + +### Key Components +1. **IPolyBus**: Main interface for message send/receive operations +2. **Transactions**: Atomic units grouping related messages with commit/abort semantics +3. **Handlers**: Middleware pipeline for incoming/outgoing message processing +4. **Transport**: Pluggable abstraction for messaging systems (RabbitMQ, Kafka, etc.) +5. **Messages**: Strongly-typed objects with metadata and headers +6. **Builder**: Fluent API for configuration + +## Language-Specific Guidelines + +### TypeScript (`src/typescript/`) + +**Technology Stack:** +- Node.js 14+, TypeScript 5.2+ +- Jest for testing, ESLint for linting +- Rollup for browser bundling, supports CommonJS, ESM, UMD + +**Coding Conventions:** +- Use async/await for all asynchronous operations +- Implement interfaces with `implements` keyword +- Use `reflect-metadata` for decorator metadata +- Export types and interfaces alongside implementations +- Prefer functional programming patterns where appropriate +- Use descriptive variable names (e.g., `transaction`, not `tx`) + +**Testing:** +- Place tests in `__tests__` directories next to source files +- Name test files: `*.test.ts` +- Use `@jest/globals` for test functions +- Mock async operations appropriately +- Maintain high coverage (run `npm test:coverage`) + +**Build System:** +- Source: `src/`, Output: `dist/` +- Multiple targets: CommonJS (`dist/index.js`), ESM (`dist/index.mjs`), UMD (`dist/index.umd.js`) +- Type definitions: `dist/index.d.ts` + +### Python (`src/python/`) + +**Technology Stack:** +- Python 3.8-3.12 +- pytest, pytest-asyncio, pytest-cov for testing +- black, isort, flake8, mypy for code quality +- setuptools for packaging + +**Coding Conventions:** +- Use type hints everywhere (enforced by mypy strict mode) +- Follow PEP 8 style guide (enforced by black/isort) +- Use `async`/`await` for asynchronous operations +- Implement abstract base classes with `ABC` and `@abstractmethod` +- Use snake_case for variables/functions, PascalCase for classes +- Document with docstrings (Google/NumPy style) + +**Type Safety:** +```python +from abc import ABC, abstractmethod +from typing import Any, Callable, List, Optional + +class IPolyBus(ABC): + @abstractmethod + async def start(self) -> None: + """Start the bus.""" + pass +``` + +**Testing:** +- Place tests in `tests/` directory (mirrors `src/` structure) +- Name test files: `test_*.py` +- Use `pytest.mark.asyncio` for async tests +- Coverage config excludes interfaces, handlers, factories, and `__init__.py` + +**Development:** +- Use `./dev.sh` script for common tasks (install, test, lint, format) +- Virtual environment recommended + +### .NET (`src/dotnet/`) + +**Technology Stack:** +- .NET Standard 2.1 (cross-platform) +- xUnit or NUnit for testing +- Solution: `PolyBus.slnx` +- Projects: `PolyBus/` (library), `PloyBus.Tests/` (tests) + +**Coding Conventions:** +- Use PascalCase for public members, camelCase for private +- Implement interfaces explicitly: `public class PolyBus : IPolyBus` +- Use `async`/`await` with `Task` return types +- Properties over fields for public APIs +- Use expression bodies for simple members +- Leverage C# modern features (pattern matching, null-coalescing, etc.) + +**Testing:** +- Test project: `PloyBus.Tests/` +- Use async test methods +- Run: `dotnet test` + +**Linting:** +- Use `./lint.sh` for code quality checks +- Follow `.editorconfig` and `.DotSettings` conventions + +## Cross-Language Consistency + +### Naming Conventions +Maintain equivalent naming across languages: + +| Concept | TypeScript | Python | .NET | +|---------|-----------|--------|------| +| Interface | `IPolyBus` | `IPolyBus` (ABC) | `IPolyBus` | +| Builder | `PolyBusBuilder` | `PolyBusBuilder` | `PolyBusBuilder` | +| Methods | `createTransaction()` | `create_transaction()` | `CreateTransaction()` | +| Properties | `incomingHandlers` | `incoming_handlers` | `IncomingHandlers` | + +### API Parity +All implementations must support: +- Same core interfaces and methods +- Async/await patterns +- Transaction commit/abort semantics +- Handler pipeline architecture +- Message metadata and headers +- Transport abstraction +- Builder pattern for configuration + +### Message Format +Messages are JSON-serializable with this structure: +```json +{ + "type": "MessageType", + "data": {}, + "headers": { + "messageId": "uuid", + "correlationId": "uuid", + "contentType": "application/json" + } +} +``` + +## Handler Pipeline Pattern + +Handlers are middleware functions that process messages: + +**TypeScript:** +```typescript +type IncomingHandler = (message: IncomingMessage, next: () => Promise) => Promise; +type OutgoingHandler = (message: Message, next: () => Promise) => Promise; +``` + +**Python:** +```python +IncomingHandler = Callable[[IncomingMessage, Callable[[], Awaitable[None]]], Awaitable[None]] +OutgoingHandler = Callable[[Message, Callable[[], Awaitable[None]]], Awaitable[None]] +``` + +**.NET:** +```csharp +public delegate Task IncomingHandler(IncomingMessage message, Func next); +public delegate Task OutgoingHandler(Message message, Func next); +``` + +**Handler Order:** +- Handlers execute in order they're added +- Each handler calls `next()` to continue the pipeline +- Error handlers wrap the pipeline for exception handling + +## Transport Interface + +Transports must implement these core methods across all languages: + +**Key Methods:** +- `start()` / `Start()` - Initialize the transport +- `stop()` / `Stop()` - Shutdown the transport +- `send(message)` / `Send(message)` - Send a message +- `createTransaction()` / `CreateTransaction()` - Begin a transaction +- `subscribe(handler)` / `Subscribe(handler)` - Register message handler + +**In-Memory Transport:** +- Default transport for testing +- Simulates message passing without external dependencies +- Located in `transport/in-memory/` directory + +## Common Patterns + +### Transaction Pattern +```typescript +const transaction = await bus.createTransaction(); +try { + transaction.addOutgoingMessage({ type: 'Event', data: {} }); + await transaction.commit(); +} catch (error) { + await transaction.abort(); +} +``` + +### Builder Pattern +```typescript +const builder = new PolyBusBuilder(); +builder.name = 'my-service'; +builder.incomingHandlers.push(deserializer); +builder.outgoingHandlers.push(serializer); +const bus = await builder.build(); +``` + +### Serialization +```typescript +const jsonHandlers = new JsonHandlers(); +builder.incomingHandlers.push(jsonHandlers.deserializer.bind(jsonHandlers)); +builder.outgoingHandlers.push(jsonHandlers.serializer.bind(jsonHandlers)); +``` + +## Error Handling + +### Error Handler Types +1. **Transaction Errors**: Handle errors during message processing +2. **Transport Errors**: Handle connection/send failures +3. **Serialization Errors**: Handle JSON parse/stringify failures + +### Implementation +Error handlers wrap the pipeline and provide: +- Retry logic +- Logging +- Dead-letter queuing +- Circuit breaker patterns + +## Testing Guidelines + +### Unit Tests +- Test individual components in isolation +- Mock dependencies (especially transport) +- Test error conditions and edge cases +- Verify handler pipeline execution order + +### Integration Tests +- Test component interactions +- Use in-memory transport for deterministic tests +- Test full message flow (send → receive → process) +- Verify transaction semantics (commit/abort) + +### Coverage Goals +- Aim for >90% code coverage +- All public APIs must have tests +- Critical paths require multiple test scenarios +- Exclude interface definitions and simple delegators + +## Documentation Standards + +### Code Comments +- Document public APIs with JSDoc/docstrings/XML comments +- Explain "why" not "what" in implementation comments +- Include usage examples for complex APIs +- Note cross-language compatibility requirements + +### README Files +- Each language has its own README in `src/{language}/` +- Include quickstart examples +- Document development setup +- List language-specific considerations + +### Examples +- Place examples in `src/{language}/examples/` +- Show common use cases +- Demonstrate best practices +- Keep examples simple and focused + +## Code Review Checklist + +When generating code: +- [ ] Follows language-specific naming conventions +- [ ] Uses async/await appropriately +- [ ] Includes error handling +- [ ] Has type annotations (TypeScript/Python) or strong typing (.NET) +- [ ] Maintains API parity with other languages +- [ ] Includes relevant tests +- [ ] Follows DRY principle +- [ ] Handles edge cases +- [ ] Documents public APIs +- [ ] Uses appropriate design patterns + +## Performance Considerations + +- **Async Operations**: All I/O must be asynchronous +- **Handler Pipeline**: Keep handlers lightweight, avoid blocking +- **Message Size**: Be mindful of large payloads +- **Connection Pooling**: Transport implementations should pool connections +- **Memory Management**: Dispose resources properly in all languages + +## Security Considerations + +- **Message Validation**: Validate incoming messages +- **Serialization**: Prevent injection attacks in JSON handlers +- **Transport Security**: Support TLS/SSL in transport implementations +- **Authentication**: Support auth mechanisms in transports +- **Authorization**: Provide hooks for message-level authorization + +## Future Considerations + +### PHP Implementation (`src/php/`) +- PHP 8.0+ with type declarations +- PSR-4 autoloading, PSR-12 coding standards +- Composer for dependency management +- PHPUnit for testing +- Async support via Amphp or ReactPHP + +### Additional Transports +- RabbitMQ (AMQP) +- Azure Service Bus +- Amazon SQS +- Apache Kafka +- Redis Pub/Sub + +## Common Commands + +### TypeScript +```bash +cd src/typescript +npm install # Install dependencies +npm run build # Build all targets +npm test # Run tests +npm run test:coverage # Coverage report +npm run lint # Check code quality +npm run lint:fix # Auto-fix linting issues +``` + +### Python +```bash +cd src/python +./dev.sh install # Setup virtual env and install +./dev.sh test # Run tests with coverage +./dev.sh lint # Check code quality +./dev.sh format # Format code (black/isort) +./dev.sh type-check # Run mypy +``` + +### .NET +```bash +cd src/dotnet +dotnet restore # Restore dependencies +dotnet build # Build solution +dotnet test # Run tests +./lint.sh # Run linting +``` + +## Resources + +- **Main README**: `/README.md` +- **Contributing Guide**: `/CONTRIBUTING.md` (if exists) +- **Documentation**: GitHub Wiki (planned) +- **Language READMEs**: `src/{language}/README.md` + +## When in Doubt + +1. **Consistency First**: Match existing patterns in the codebase +2. **Check Other Languages**: See how feature is implemented in other languages +3. **Test Everything**: Write tests before/during implementation +4. **Document APIs**: All public interfaces need documentation +5. **Ask Questions**: Better to clarify than implement incorrectly diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml new file mode 100644 index 0000000..ff4d20e --- /dev/null +++ b/.github/workflows/pr-tests.yml @@ -0,0 +1,122 @@ +name: PR Tests with Coverage + +on: + pull_request: + branches: + - main + - develop + paths: + - 'src/**' + - '.github/workflows/pr-tests.yml' + +jobs: + dotnet-tests: + name: .NET Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: src/dotnet + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - name: Run tests with coverage + run: dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --results-directory ./coverage + + - name: Upload .NET coverage + uses: codecov/codecov-action@v4 + with: + files: ./src/dotnet/coverage/**/coverage.cobertura.xml + flags: dotnet + name: dotnet-coverage + + python-tests: + name: Python Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: src/python + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests with coverage + run: pytest --cov=src --cov-report=xml --cov-report=term-missing + + - name: Upload Python coverage + uses: codecov/codecov-action@v4 + with: + files: ./src/python/coverage.xml + flags: python + name: python-coverage + + typescript-tests: + name: TypeScript Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: src/typescript + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: src/typescript/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run tests with coverage + run: npm run test:ci + + - name: Upload TypeScript coverage + uses: codecov/codecov-action@v4 + with: + files: ./src/typescript/coverage/coverage-final.json + flags: typescript + name: typescript-coverage + + coverage-summary: + name: Coverage Summary + runs-on: ubuntu-latest + needs: [dotnet-tests, python-tests, typescript-tests] + if: always() + + steps: + - name: Summary + run: | + echo "## Test Coverage Summary" >> $GITHUB_STEP_SUMMARY + echo "All project tests have completed. Check individual job results for details." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- ✅ .NET Tests: ${{ needs.dotnet-tests.result }}" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Python Tests: ${{ needs.python-tests.result }}" >> $GITHUB_STEP_SUMMARY + echo "- ✅ TypeScript Tests: ${{ needs.typescript-tests.result }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release-and-publish.yml b/.github/workflows/release-and-publish.yml new file mode 100644 index 0000000..d620b4a --- /dev/null +++ b/.github/workflows/release-and-publish.yml @@ -0,0 +1,313 @@ +name: Release and Publish Packages + +on: + push: + branches: + - main + paths: + - 'src/**' + - '.github/workflows/release-and-publish.yml' + +permissions: + contents: write + packages: write + +jobs: + # Determine version bump type from commit messages + determine-version: + name: Determine Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.semver.outputs.version }} + version_tag: ${{ steps.semver.outputs.version_tag }} + should_release: ${{ steps.semver.outputs.should_release }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for proper versioning + + - name: Get latest tag + id: get_tag + run: | + # Get the latest tag, or use 0.0.0 if no tags exist + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + echo "latest_tag=${LATEST_TAG}" >> $GITHUB_OUTPUT + echo "Latest tag: ${LATEST_TAG}" + + - name: Determine version bump + id: semver + run: | + LATEST_TAG="${{ steps.get_tag.outputs.latest_tag }}" + + # Remove 'v' prefix if present + CURRENT_VERSION=${LATEST_TAG#v} + + # Get commit messages since last tag + if [ "$LATEST_TAG" = "v0.0.0" ]; then + COMMITS=$(git log --pretty=format:"%s" HEAD) + else + COMMITS=$(git log ${LATEST_TAG}..HEAD --pretty=format:"%s") + fi + + echo "Analyzing commits:" + echo "$COMMITS" + + # Determine bump type based on conventional commits + BUMP_TYPE="patch" + + if echo "$COMMITS" | grep -qiE "^(BREAKING CHANGE|.*!:)"; then + BUMP_TYPE="major" + echo "Found breaking change - major version bump" + elif echo "$COMMITS" | grep -qiE "^feat(\(.*\))?:"; then + BUMP_TYPE="minor" + echo "Found feature - minor version bump" + elif echo "$COMMITS" | grep -qiE "^(fix|perf|refactor|docs|style|test|chore)(\(.*\))?:"; then + BUMP_TYPE="patch" + echo "Found fix/patch - patch version bump" + else + echo "No conventional commit found - skipping release" + echo "should_release=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Parse version + IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION" + MAJOR="${VERSION_PARTS[0]}" + MINOR="${VERSION_PARTS[1]}" + PATCH="${VERSION_PARTS[2]}" + + # Bump version + case $BUMP_TYPE in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + esac + + NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" + NEW_TAG="v${NEW_VERSION}" + + echo "New version: ${NEW_VERSION}" + echo "New tag: ${NEW_TAG}" + + echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT + echo "version_tag=${NEW_TAG}" >> $GITHUB_OUTPUT + echo "should_release=true" >> $GITHUB_OUTPUT + + # Build and test all packages + build-and-test: + name: Build and Test All Packages + runs-on: ubuntu-latest + needs: determine-version + if: needs.determine-version.outputs.should_release == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # TypeScript build and test + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: src/typescript/package-lock.json + + - name: Build TypeScript package + working-directory: src/typescript + run: | + npm ci + npm run build + npm test + + # Python build and test + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Build Python package + working-directory: src/python + run: | + python -m pip install --upgrade pip + pip install build twine + pip install -e ".[dev]" + pytest + python -m build + + # .NET build and test + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Build .NET package + working-directory: src/dotnet + run: | + dotnet restore + dotnet build --configuration Release + dotnet test --configuration Release + dotnet pack --configuration Release --output ./nupkg + + # Upload build artifacts + - name: Upload TypeScript artifact + uses: actions/upload-artifact@v4 + with: + name: typescript-package + path: src/typescript/dist/ + + - name: Upload Python artifact + uses: actions/upload-artifact@v4 + with: + name: python-package + path: src/python/dist/ + + - name: Upload .NET artifact + uses: actions/upload-artifact@v4 + with: + name: dotnet-package + path: src/dotnet/PolyBus/nupkg/ + + # Publish to npm + publish-npm: + name: Publish to npm + runs-on: ubuntu-latest + needs: [determine-version, build-and-test] + if: needs.determine-version.outputs.should_release == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Download TypeScript artifact + uses: actions/download-artifact@v4 + with: + name: typescript-package + path: src/typescript/dist/ + + - name: Update package version + working-directory: src/typescript + run: | + npm version ${{ needs.determine-version.outputs.version }} --no-git-tag-version + + - name: Publish to npm + working-directory: src/typescript + run: | + npm ci + npm run build + npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + # Publish to PyPI + publish-pypi: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: [determine-version, build-and-test] + if: needs.determine-version.outputs.should_release == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Update package version + working-directory: src/python + run: | + # Update version in pyproject.toml + sed -i "s/^version = .*/version = \"${{ needs.determine-version.outputs.version }}\"/" pyproject.toml + + - name: Build package + working-directory: src/python + run: | + python -m pip install --upgrade pip + pip install build twine + python -m build + + - name: Publish to PyPI + working-directory: src/python + run: | + python -m twine upload dist/* --skip-existing + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + + # Publish to NuGet + publish-nuget: + name: Publish to NuGet + runs-on: ubuntu-latest + needs: [determine-version, build-and-test] + if: needs.determine-version.outputs.should_release == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Update package version + working-directory: src/dotnet/PolyBus + run: | + # Update version in csproj + sed -i "s/.*<\/Version>/${{ needs.determine-version.outputs.version }}<\/Version>/" PolyBus.csproj + + - name: Build and pack + working-directory: src/dotnet + run: | + dotnet restore + dotnet build --configuration Release + dotnet pack --configuration Release --output ./nupkg + + - name: Publish to NuGet + working-directory: src/dotnet + run: | + dotnet nuget push "./nupkg/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + + # Summary job + publish-summary: + name: Publish Summary + runs-on: ubuntu-latest + needs: [determine-version, publish-npm, publish-pypi, publish-nuget] + if: always() && needs.determine-version.outputs.should_release == 'true' + steps: + - name: Summary + run: | + echo "## 🚀 Release ${{ needs.determine-version.outputs.version_tag }} Published" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Package Status" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- 📦 **npm**: ${{ needs.publish-npm.result }}" >> $GITHUB_STEP_SUMMARY + echo "- 🐍 **PyPI**: ${{ needs.publish-pypi.result }}" >> $GITHUB_STEP_SUMMARY + echo "- ⚡ **NuGet**: ${{ needs.publish-nuget.result }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Installation Commands" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "# TypeScript/JavaScript" >> $GITHUB_STEP_SUMMARY + echo "npm install poly-bus@${{ needs.determine-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "# Python" >> $GITHUB_STEP_SUMMARY + echo "pip install poly-bus==${{ needs.determine-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "# .NET" >> $GITHUB_STEP_SUMMARY + echo "dotnet add package PolyBus --version ${{ needs.determine-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd3d86c --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +**/.idea/** +**/bin/** +**/coverage/** +**/dist/** +**/node_modules/** +**/obj/** +**/nupkg/** +**/__pycache__/** +*.pyc +.DS_Store +._* +*.user +.venv/ +venv/ + +# Build artifacts +*.egg-info/ +build/ +*.whl + +# npm +.npmrc.local +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment +.env +.env.local diff --git a/README.md b/README.md new file mode 100644 index 0000000..3effd7c --- /dev/null +++ b/README.md @@ -0,0 +1,280 @@ +# PolyBus + +**A polyglot messaging framework for building interoperable applications across multiple programming languages.** + +PolyBus provides a unified interface for sending and receiving messages between applications written in different languages. Whether you're building microservices, distributed systems, or integrating legacy applications, PolyBus enables seamless communication across your polyglot stack. + +## 🌟 Key Features + +- **🔄 Multi-Language Support**: Native implementations for TypeScript, Python, and .NET +- **🚀 Flexible Transport**: Pluggable transport layer supporting various messaging systems +- **⚡ Async/Await**: Modern asynchronous APIs in all language implementations +- **🔌 Middleware Pipeline**: Extensible handler chains for incoming and outgoing messages +- **📦 Transaction Support**: Atomic message operations with commit/abort semantics +- **🎯 Type Safety**: Strong typing and interface definitions across all platforms +- **🛠️ Builder Pattern**: Fluent API for easy configuration +- **📝 Message Metadata**: Rich message headers and routing information +- **🔁 Serialization**: Built-in JSON serialization with customizable handlers +- **⚠️ Error Handling**: Comprehensive error handlers for reliable message processing + +## 📋 Supported Languages + +| Language | Version | Status | Package | +|----------|---------|--------|---------| +| **TypeScript/JavaScript** | Node.js 14+ | ✅ Stable | CommonJS, ESM, UMD | +| **Python** | 3.8-3.12 | ✅ Stable | PyPI package | +| **.NET** | .NET Standard 2.1 | ✅ Stable | NuGet package | +| **PHP** | 8.0+ | 🚧 Planned | - | + +## 🚀 Quick Start + +### TypeScript + +```typescript +import { PolyBusBuilder, JsonHandlers } from 'poly-bus'; + +// Configure the bus +const builder = new PolyBusBuilder(); +builder.name = 'my-service'; + +// Add JSON serialization +const jsonHandlers = new JsonHandlers(); +builder.incomingHandlers.push(jsonHandlers.deserializer.bind(jsonHandlers)); +builder.outgoingHandlers.push(jsonHandlers.serializer.bind(jsonHandlers)); + +// Build and start +const bus = await builder.build(); +await bus.start(); + +// Send a message +const transaction = await bus.createTransaction(); +transaction.addOutgoingMessage({ type: 'UserCreated', userId: 123 }); +await transaction.commit(); + +await bus.stop(); +``` + +### Python + +```python +from poly_bus import PolyBusBuilder, JsonHandlers + +# Configure the bus +builder = PolyBusBuilder() +builder.name = 'my-service' + +# Add JSON serialization +json_handlers = JsonHandlers() +builder.incoming_handlers.append(json_handlers.deserializer) +builder.outgoing_handlers.append(json_handlers.serializer) + +# Build and start +bus = await builder.build() +await bus.start() + +# Send a message +transaction = await bus.create_transaction() +transaction.add_outgoing_message({'type': 'UserCreated', 'user_id': 123}) +await transaction.commit() + +await bus.stop() +``` + +### .NET + +```csharp +using PolyBus; +using PolyBus.Transport.Transactions.Messages.Handlers; + +// Configure the bus +var builder = new PolyBusBuilder +{ + Name = "my-service" +}; + +// Add JSON serialization +var jsonHandlers = new JsonHandlers(); +builder.IncomingHandlers.Add(jsonHandlers.Deserializer); +builder.OutgoingHandlers.Add(jsonHandlers.Serializer); + +// Build and start +var bus = await builder.Build(); +await bus.Start(); + +// Send a message +var transaction = await bus.CreateTransaction(); +transaction.AddOutgoingMessage(new { Type = "UserCreated", UserId = 123 }); +await transaction.Commit(); + +await bus.Stop(); +``` + +## 📚 Documentation + +For comprehensive documentation, examples, and detailed guides, please visit the [**PolyBus Wiki**](https://github.com/CyAScott/poly-bus/wiki). + +### Documentation Topics + +- **[Getting Started Guide](https://github.com/CyAScott/poly-bus/wiki/Getting-Started)** - Installation and basic setup +- **[Core Concepts](https://github.com/CyAScott/poly-bus/wiki/Core-Concepts)** - Understanding transactions, handlers, and transports +- **[API Reference](https://github.com/CyAScott/poly-bus/wiki/API-Reference)** - Complete API documentation for all languages +- **[Transport Implementations](https://github.com/CyAScott/poly-bus/wiki/Transports)** - Available transports and custom transport development +- **[Handlers & Middleware](https://github.com/CyAScott/poly-bus/wiki/Handlers)** - Building custom handlers and middleware +- **[Message Serialization](https://github.com/CyAScott/poly-bus/wiki/Serialization)** - JSON, custom serializers, and content types +- **[Error Handling](https://github.com/CyAScott/poly-bus/wiki/Error-Handling)** - Error handlers and recovery strategies +- **[Advanced Topics](https://github.com/CyAScott/poly-bus/wiki/Advanced)** - Delayed messages, subscriptions, and patterns +- **[Examples](https://github.com/CyAScott/poly-bus/wiki/Examples)** - Real-world usage examples and patterns +- **[Migration Guides](https://github.com/CyAScott/poly-bus/wiki/Migration)** - Upgrading between versions + +## 🏗️ Architecture + +PolyBus follows a clean, layered architecture: + +``` +┌─────────────────────────────────────┐ +│ Application Code │ +├─────────────────────────────────────┤ +│ IPolyBus API │ +├─────────────────────────────────────┤ +│ Handler Pipeline (Middleware) │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ Incoming │ │ Outgoing │ │ +│ │ Handlers │ │ Handlers │ │ +│ └──────────┘ └──────────┘ │ +├─────────────────────────────────────┤ +│ Transaction Management │ +├─────────────────────────────────────┤ +│ Transport Interface │ +│ (ITransport abstraction) │ +├─────────────────────────────────────┤ +│ Transport Implementations │ +│ (RabbitMQ, Kafka, SQS, etc.) │ +└─────────────────────────────────────┘ +``` + +### Core Components + +- **IPolyBus**: Main interface providing message send/receive operations +- **Transactions**: Atomic units of work for grouping related messages +- **Handlers**: Middleware pipeline for processing incoming/outgoing messages +- **Transport**: Pluggable abstraction for different messaging systems +- **Messages**: Strongly-typed message objects with metadata and headers +- **Builder**: Fluent API for configuration and dependency injection + +## 🔌 Transport Support + +PolyBus supports a pluggable transport architecture. Current and planned transports: + +- ✅ **In-Memory Transport** - For testing and same-process communication +- 🚧 **RabbitMQ** - AMQP-based messaging +- 🚧 **Azure Service Bus** - Cloud-native messaging +- 🚧 **Amazon SQS** - AWS message queuing +- 🚧 **Apache Kafka** - Distributed streaming +- 🚧 **Redis Pub/Sub** - Lightweight messaging + +Custom transports can be implemented by extending the `ITransport` interface. + +## 🛠️ Development + +Each language implementation has its own development setup: + +### TypeScript Development + +```bash +cd src/typescript +npm install +npm run build # Build the project +npm test # Run tests +npm run lint # Check code quality +``` + +See [TypeScript README](src/typescript/README.md) for detailed development instructions. + +### Python Development + +```bash +cd src/python +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +./dev.sh install +./dev.sh test +./dev.sh lint +``` + +See [Python README](src/python/README.md) for detailed development instructions. + +### .NET Development + +```bash +cd src/dotnet +dotnet restore +dotnet build +dotnet test +./lint.sh +``` + +See [.NET README](src/dotnet/README.md) for detailed development instructions. + +## 🧪 Testing + +All implementations include comprehensive test suites: + +- **Unit Tests**: Testing individual components in isolation +- **Integration Tests**: Testing component interactions +- **Code Coverage**: Maintaining high coverage standards +- **Type Safety**: Static analysis and type checking + +## 📦 Installation + +### TypeScript/JavaScript + +```bash +npm install poly-bus +# or +yarn add poly-bus +``` + +### Python + +```bash +pip install poly-bus +``` + +### .NET + +```bash +dotnet add package PolyBus +``` + +## 🤝 Contributing + +We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details on: + +- Code style and conventions +- Testing requirements +- Pull request process +- Development setup + +### Releasing + +This project uses automated semantic versioning and publishing. See: +- **[Quick Release Guide](RELEASE.md)** - Quick reference for developers +- **[Publishing Setup](PUBLISHING.md)** - Complete setup and troubleshooting guide +- **[Changelog](CHANGELOG.md)** - Version history + +## 📄 License + +This project is licensed under the terms specified in the [LICENSE](LICENSE) file. + +## 🔗 Links + +- **Documentation**: [GitHub Wiki](https://github.com/CyAScott/poly-bus/wiki) +- **Issues**: [GitHub Issues](https://github.com/CyAScott/poly-bus/issues) +- **Discussions**: [GitHub Discussions](https://github.com/CyAScott/poly-bus/discussions) +- **Changelog**: [CHANGELOG.md](CHANGELOG.md) + +## ⭐ Project Status + +PolyBus is actively maintained and production-ready for TypeScript, Python, and .NET implementations. PHP support is planned for future releases. + +If you find PolyBus useful, please consider giving it a star ⭐ on GitHub! \ No newline at end of file diff --git a/scripts/version.sh b/scripts/version.sh new file mode 100755 index 0000000..0498dbb --- /dev/null +++ b/scripts/version.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash + +# Version Management Script for PolyBus +# This script helps sync versions across all package configurations + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Files to update +TYPESCRIPT_PACKAGE="$ROOT_DIR/src/typescript/package.json" +PYTHON_PYPROJECT="$ROOT_DIR/src/python/pyproject.toml" +DOTNET_CSPROJ="$ROOT_DIR/src/dotnet/PolyBus/PolyBus.csproj" + +print_usage() { + echo "Usage: $0 [command] [version]" + echo "" + echo "Commands:" + echo " get - Display current versions" + echo " set - Set version across all packages" + echo " check - Check if all versions are in sync" + echo " bump - Bump version by type" + echo "" + echo "Examples:" + echo " $0 get" + echo " $0 set 1.2.3" + echo " $0 bump minor" + echo " $0 check" +} + +get_typescript_version() { + if [ -f "$TYPESCRIPT_PACKAGE" ]; then + grep -o '"version": "[^"]*"' "$TYPESCRIPT_PACKAGE" | cut -d'"' -f4 + else + echo "N/A" + fi +} + +get_python_version() { + if [ -f "$PYTHON_PYPROJECT" ]; then + grep '^version = ' "$PYTHON_PYPROJECT" | cut -d'"' -f2 + else + echo "N/A" + fi +} + +get_dotnet_version() { + if [ -f "$DOTNET_CSPROJ" ]; then + grep '' "$DOTNET_CSPROJ" | sed 's/.*\(.*\)<\/Version>.*/\1/' | tr -d ' ' + else + echo "N/A" + fi +} + +display_versions() { + echo -e "${BLUE}Current Package Versions:${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo -e "TypeScript: ${GREEN}$(get_typescript_version)${NC}" + echo -e "Python: ${GREEN}$(get_python_version)${NC}" + echo -e ".NET: ${GREEN}$(get_dotnet_version)${NC}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +} + +check_versions_sync() { + TS_VERSION=$(get_typescript_version) + PY_VERSION=$(get_python_version) + NET_VERSION=$(get_dotnet_version) + + if [ "$TS_VERSION" = "$PY_VERSION" ] && [ "$PY_VERSION" = "$NET_VERSION" ]; then + echo -e "${GREEN}✓ All versions are in sync: ${TS_VERSION}${NC}" + return 0 + else + echo -e "${RED}✗ Versions are out of sync!${NC}" + display_versions + return 1 + fi +} + +validate_semver() { + local version=$1 + if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo -e "${RED}Error: Invalid semantic version format. Expected: X.Y.Z${NC}" + return 1 + fi + return 0 +} + +set_version() { + local version=$1 + + if ! validate_semver "$version"; then + exit 1 + fi + + echo -e "${BLUE}Setting version to ${version} across all packages...${NC}" + echo "" + + # Update TypeScript package.json + if [ -f "$TYPESCRIPT_PACKAGE" ]; then + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"${version}\"/" "$TYPESCRIPT_PACKAGE" + else + # Linux + sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${version}\"/" "$TYPESCRIPT_PACKAGE" + fi + echo -e "${GREEN}✓ Updated TypeScript package.json${NC}" + fi + + # Update Python pyproject.toml + if [ -f "$PYTHON_PYPROJECT" ]; then + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s/^version = .*/version = \"${version}\"/" "$PYTHON_PYPROJECT" + else + sed -i "s/^version = .*/version = \"${version}\"/" "$PYTHON_PYPROJECT" + fi + echo -e "${GREEN}✓ Updated Python pyproject.toml${NC}" + fi + + # Update .NET csproj + if [ -f "$DOTNET_CSPROJ" ]; then + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|.*|${version}|" "$DOTNET_CSPROJ" + else + sed -i "s|.*|${version}|" "$DOTNET_CSPROJ" + fi + echo -e "${GREEN}✓ Updated .NET PolyBus.csproj${NC}" + fi + + echo "" + display_versions +} + +bump_version() { + local bump_type=$1 + local current_version=$(get_typescript_version) + + if [ "$current_version" = "N/A" ]; then + echo -e "${RED}Error: Cannot determine current version${NC}" + exit 1 + fi + + IFS='.' read -r -a version_parts <<< "$current_version" + local major="${version_parts[0]}" + local minor="${version_parts[1]}" + local patch="${version_parts[2]}" + + case $bump_type in + major) + major=$((major + 1)) + minor=0 + patch=0 + ;; + minor) + minor=$((minor + 1)) + patch=0 + ;; + patch) + patch=$((patch + 1)) + ;; + *) + echo -e "${RED}Error: Invalid bump type. Use: major, minor, or patch${NC}" + exit 1 + ;; + esac + + local new_version="${major}.${minor}.${patch}" + echo -e "${YELLOW}Bumping ${bump_type} version: ${current_version} → ${new_version}${NC}" + echo "" + + set_version "$new_version" +} + +# Main script logic +case "${1:-}" in + get) + display_versions + ;; + set) + if [ -z "${2:-}" ]; then + echo -e "${RED}Error: Version argument required${NC}" + print_usage + exit 1 + fi + set_version "$2" + ;; + check) + check_versions_sync + ;; + bump) + if [ -z "${2:-}" ]; then + echo -e "${RED}Error: Bump type required (major|minor|patch)${NC}" + print_usage + exit 1 + fi + bump_version "$2" + ;; + help|--help|-h) + print_usage + ;; + *) + echo -e "${RED}Error: Invalid command${NC}" + echo "" + print_usage + exit 1 + ;; +esac diff --git a/src/dotnet/.editorconfig b/src/dotnet/.editorconfig new file mode 100644 index 0000000..f7599bb --- /dev/null +++ b/src/dotnet/.editorconfig @@ -0,0 +1,955 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# Code files +[*.{cs,csx,vb,vbx}] +indent_style = space +indent_size = 4 + +# Project files +[*.{csproj,vbproj,vcxproj,proj,projitems,shproj}] +indent_style = space +indent_size = 2 + +# Config files +[*.{json,xml,yml,yaml}] +indent_style = space +indent_size = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false + +# C# files +[*.cs] + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +csharp_indent_block_contents = true +csharp_indent_braces = false + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_after_comma = true +csharp_space_after_dot = false + +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + +# Organize usings +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# this. preferences +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +dotnet_style_readonly_field = true:warning + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent + +# C# Coding Conventions +# var preferences +csharp_style_var_for_built_in_types = false:warning +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:warning + +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_braces = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# Expression-level preferences +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Namespace preferences +csharp_style_namespace_declarations = file_scoped:error + +# Naming conventions +# Async methods should have "Async" suffix +dotnet_naming_rule.async_methods_end_in_async.severity = warning +dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods +dotnet_naming_rule.async_methods_end_in_async.style = end_in_async + +dotnet_naming_symbols.any_async_methods.applicable_kinds = method +dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * +dotnet_naming_symbols.any_async_methods.required_modifiers = async + +dotnet_naming_style.end_in_async.required_suffix = Async +dotnet_naming_style.end_in_async.capitalization = pascal_case + +# Interfaces should be prefixed with I +dotnet_naming_rule.interface_should_be_prefixed_with_i.severity = warning +dotnet_naming_rule.interface_should_be_prefixed_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_prefixed_with_i.style = prefixed_with_i + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected + +dotnet_naming_style.prefixed_with_i.required_prefix = I +dotnet_naming_style.prefixed_with_i.capitalization = pascal_case + +# Types should be PascalCase +dotnet_naming_rule.types_should_be_pascal_case.severity = warning +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum + +dotnet_naming_style.pascal_case.capitalization = pascal_case + +# Non-field members should be PascalCase +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method + +# Constants should be PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = warning +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = pascal_case + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +# Static readonly fields should be PascalCase +dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.symbols = static_readonly_fields +dotnet_naming_rule.static_readonly_fields_should_be_pascal_case.style = pascal_case + +dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly + +# Private fields should be camelCase with underscore prefix +dotnet_naming_rule.private_fields_should_be_camel_case_with_underscore.severity = warning +dotnet_naming_rule.private_fields_should_be_camel_case_with_underscore.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_camel_case_with_underscore.style = camel_case_with_underscore + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.camel_case_with_underscore.required_prefix = _ +dotnet_naming_style.camel_case_with_underscore.capitalization = camel_case + +# Public fields should be PascalCase +dotnet_naming_rule.public_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.public_fields_should_be_pascal_case.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascal_case.style = pascal_case + +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal, protected, protected_internal, private_protected + +# Locals and parameters should be camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = warning +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case.capitalization = camel_case + +# .NET Code Quality Rules +dotnet_analyzer_diagnostic.CA1000.severity = warning # Do not declare static members on generic types +dotnet_analyzer_diagnostic.CA1001.severity = warning # Types that own disposable fields should be disposable +dotnet_analyzer_diagnostic.CA1002.severity = warning # Do not expose generic lists +dotnet_analyzer_diagnostic.CA1003.severity = warning # Use generic event handler instances +dotnet_analyzer_diagnostic.CA1005.severity = warning # Avoid excessive parameters on generic types +dotnet_analyzer_diagnostic.CA1008.severity = warning # Enums should have zero value +dotnet_analyzer_diagnostic.CA1010.severity = warning # Collections should implement generic interface +dotnet_analyzer_diagnostic.CA1012.severity = warning # Abstract types should not have constructors +dotnet_analyzer_diagnostic.CA1014.severity = silent # Mark assemblies with CLSCompliant +dotnet_analyzer_diagnostic.CA1016.severity = warning # Mark assemblies with assembly version +dotnet_analyzer_diagnostic.CA1017.severity = warning # Mark assemblies with ComVisible +dotnet_analyzer_diagnostic.CA1018.severity = warning # Mark attributes with AttributeUsageAttribute +dotnet_analyzer_diagnostic.CA1019.severity = warning # Define accessors for attribute arguments +dotnet_analyzer_diagnostic.CA1024.severity = warning # Use properties where appropriate +dotnet_analyzer_diagnostic.CA1027.severity = warning # Mark enums with FlagsAttribute +dotnet_analyzer_diagnostic.CA1028.severity = warning # Enum Storage should be Int32 +dotnet_analyzer_diagnostic.CA1030.severity = warning # Use events where appropriate +# Style rules - make less strict +dotnet_analyzer_diagnostic.IDE0160.severity = error # Convert to file scoped namespace (enforce this) +dotnet_analyzer_diagnostic.IDE0028.severity = suggestion # Collection initialization can be simplified +dotnet_analyzer_diagnostic.IDE0022.severity = silent # Use expression body for methods +dotnet_analyzer_diagnostic.IDE0007.severity = error # Use 'var' instead of explicit type +dotnet_analyzer_diagnostic.IDE0008.severity = silent # Use explicit type instead of 'var' +dotnet_analyzer_diagnostic.IDE0040.severity = none # Accessibility modifiers required +dotnet_analyzer_diagnostic.IDE0055.severity = suggestion # Fix formatting +dotnet_analyzer_diagnostic.IDE0058.severity = suggestion # Expression value is never used +dotnet_analyzer_diagnostic.IDE0060.severity = suggestion # Remove unused parameter +dotnet_analyzer_diagnostic.IDE1006.severity = silent # Naming rule violation + +# Quality rules - make some less strict +dotnet_analyzer_diagnostic.CA1716.severity = suggestion # Identifiers should not match keywords +dotnet_analyzer_diagnostic.CA1724.severity = suggestion # Type names should not match namespaces +dotnet_analyzer_diagnostic.CA1852.severity = suggestion # Type can be sealed +dotnet_analyzer_diagnostic.CA1001.severity = suggestion # Types that own disposable fields should be disposable +dotnet_analyzer_diagnostic.CA1848.severity = suggestion # Use the LoggerMessage delegates +dotnet_analyzer_diagnostic.CA2254.severity = suggestion # Template should be a static expression +dotnet_analyzer_diagnostic.CA1305.severity = suggestion # Specify IFormatProvider + +# CS1591: Missing XML comment for publicly visible type or member +dotnet_analyzer_diagnostic.CS1591.severity = silent + +# CA1062: Review unused parameters +dotnet_analyzer_diagnostic.CA1062.severity = suggestion + +# CA1031: Do not catch general exception types +dotnet_analyzer_diagnostic.CA1031.severity = suggestion +dotnet_analyzer_diagnostic.CA1032.severity = warning # Implement standard exception constructors +dotnet_analyzer_diagnostic.CA1033.severity = warning # Interface methods should be callable by child types +dotnet_analyzer_diagnostic.CA1034.severity = warning # Nested types should not be visible +dotnet_analyzer_diagnostic.CA1036.severity = warning # Override methods on comparable types +dotnet_analyzer_diagnostic.CA1040.severity = warning # Avoid empty interfaces +dotnet_analyzer_diagnostic.CA1041.severity = warning # Provide ObsoleteAttribute message +dotnet_analyzer_diagnostic.CA1043.severity = warning # Use Integral Or String Argument For Indexers +dotnet_analyzer_diagnostic.CA1044.severity = warning # Properties should not be write only +dotnet_analyzer_diagnostic.CA1045.severity = warning # Do not pass types by reference +dotnet_analyzer_diagnostic.CA1046.severity = warning # Do not overload equality operator on reference types +dotnet_analyzer_diagnostic.CA1047.severity = warning # Do not declare protected member in sealed type +dotnet_analyzer_diagnostic.CA1050.severity = warning # Declare types in namespaces +dotnet_analyzer_diagnostic.CA1051.severity = warning # Do not declare visible instance fields +dotnet_analyzer_diagnostic.CA1052.severity = warning # Static holder types should be Static or NotInheritable +dotnet_analyzer_diagnostic.CA1054.severity = warning # URI-like parameters should not be strings +dotnet_analyzer_diagnostic.CA1055.severity = warning # URI-like return values should not be strings +dotnet_analyzer_diagnostic.CA1056.severity = warning # URI-like properties should not be strings +dotnet_analyzer_diagnostic.CA1058.severity = warning # Types should not extend certain base types +dotnet_analyzer_diagnostic.CA1060.severity = warning # Move pinvokes to native methods class +dotnet_analyzer_diagnostic.CA1061.severity = warning # Do not hide base class methods +dotnet_analyzer_diagnostic.CA1062.severity = suggestion # Validate arguments of public methods +dotnet_analyzer_diagnostic.CA1063.severity = warning # Implement IDisposable Correctly +dotnet_analyzer_diagnostic.CA1064.severity = warning # Exceptions should be public +dotnet_analyzer_diagnostic.CA1065.severity = warning # Do not raise exceptions in unexpected locations +dotnet_analyzer_diagnostic.CA1066.severity = warning # Type {0} should implement IEquatable because it overrides Equals +dotnet_analyzer_diagnostic.CA1067.severity = warning # Override Object.Equals(object) when implementing IEquatable +dotnet_analyzer_diagnostic.CA1068.severity = warning # CancellationToken parameters must come last +dotnet_analyzer_diagnostic.CA1069.severity = warning # Enums values should not be duplicated +dotnet_analyzer_diagnostic.CA1070.severity = warning # Do not declare event fields as virtual + +# CA1200: Avoid using cref tags with a prefix +dotnet_analyzer_diagnostic.CA1200.severity = warning + +# CA1303: Do not pass literals as localized parameters +dotnet_analyzer_diagnostic.CA1303.severity = silent + +# CA1304: Specify CultureInfo +dotnet_analyzer_diagnostic.CA1304.severity = warning + +# CA1305: Specify IFormatProvider +dotnet_analyzer_diagnostic.CA1305.severity = warning + +# CA1307: Specify StringComparison +dotnet_analyzer_diagnostic.CA1307.severity = warning + +# CA1308: Normalize strings to uppercase +dotnet_analyzer_diagnostic.CA1308.severity = warning + +# CA1309: Use ordinal stringcomparison +dotnet_analyzer_diagnostic.CA1309.severity = warning + +# CA1310: Specify StringComparison for correctness +dotnet_analyzer_diagnostic.CA1310.severity = warning + +# CA1501: Avoid excessive inheritance +dotnet_analyzer_diagnostic.CA1501.severity = warning + +# CA1502: Avoid excessive complexity +dotnet_analyzer_diagnostic.CA1502.severity = warning + +# CA1505: Avoid unmaintainable code +dotnet_analyzer_diagnostic.CA1505.severity = warning + +# CA1506: Avoid excessive class coupling +dotnet_analyzer_diagnostic.CA1506.severity = warning + +# CA1507: Use nameof to express symbol names +dotnet_analyzer_diagnostic.CA1507.severity = warning + +# CA1508: Avoid dead conditional code +dotnet_analyzer_diagnostic.CA1508.severity = warning + +# CA1509: Invalid entry in code metrics rule specification file +dotnet_analyzer_diagnostic.CA1509.severity = warning + +# CA1700: Do not name enum values 'Reserved' +dotnet_analyzer_diagnostic.CA1700.severity = warning + +# CA1707: Identifiers should not contain underscores +dotnet_analyzer_diagnostic.CA1707.severity = warning + +# CA1708: Identifiers should differ by more than case +dotnet_analyzer_diagnostic.CA1708.severity = warning + +# CA1710: Identifiers should have correct suffix +dotnet_analyzer_diagnostic.CA1710.severity = warning + +# CA1711: Identifiers should not have incorrect suffix +dotnet_analyzer_diagnostic.CA1711.severity = warning + +# CA1712: Do not prefix enum values with type name +dotnet_analyzer_diagnostic.CA1712.severity = warning + +# CA1713: Events should not have 'Before' or 'After' prefix +dotnet_analyzer_diagnostic.CA1713.severity = warning + +# CA1714: Flags enums should have plural names +dotnet_analyzer_diagnostic.CA1714.severity = warning + +# CA1715: Identifiers should have correct prefix +dotnet_analyzer_diagnostic.CA1715.severity = warning + +# CA1716: Identifiers should not match keywords +dotnet_analyzer_diagnostic.CA1716.severity = warning + +# CA1717: Only FlagsAttribute enums should have plural names +dotnet_analyzer_diagnostic.CA1717.severity = warning + +# CA1720: Identifier contains type name +dotnet_analyzer_diagnostic.CA1720.severity = warning + +# CA1721: Property names should not match get methods +dotnet_analyzer_diagnostic.CA1721.severity = warning + +# CA1724: Type names should not match namespaces +dotnet_analyzer_diagnostic.CA1724.severity = warning + +# CA1725: Parameter names should match base declaration +dotnet_analyzer_diagnostic.CA1725.severity = warning + +# CA1801: Review unused parameters +dotnet_analyzer_diagnostic.CA1801.severity = warning + +# CA1802: Use literals where appropriate +dotnet_analyzer_diagnostic.CA1802.severity = warning + +# CA1805: Do not initialize unnecessarily +dotnet_analyzer_diagnostic.CA1805.severity = warning + +# CA1806: Do not ignore method results +dotnet_analyzer_diagnostic.CA1806.severity = warning + +# CA1810: Initialize reference type static fields inline +dotnet_analyzer_diagnostic.CA1810.severity = warning + +# CA1812: Avoid uninstantiated internal classes +dotnet_analyzer_diagnostic.CA1812.severity = warning + +# CA1813: Avoid unsealed attributes +dotnet_analyzer_diagnostic.CA1813.severity = warning + +# CA1814: Prefer jagged arrays over multidimensional +dotnet_analyzer_diagnostic.CA1814.severity = warning + +# CA1815: Override equals and operator equals on value types +dotnet_analyzer_diagnostic.CA1815.severity = warning + +# CA1816: Dispose methods should call SuppressFinalize +dotnet_analyzer_diagnostic.CA1816.severity = warning + +# CA1819: Properties should not return arrays +dotnet_analyzer_diagnostic.CA1819.severity = warning + +# CA1820: Test for empty strings using string length +dotnet_analyzer_diagnostic.CA1820.severity = warning + +# CA1821: Remove empty Finalizers +dotnet_analyzer_diagnostic.CA1821.severity = warning + +# CA1822: Mark members as static +dotnet_analyzer_diagnostic.CA1822.severity = suggestion + +# CA1823: Avoid unused private fields +dotnet_analyzer_diagnostic.CA1823.severity = warning + +# CA1824: Mark assemblies with NeutralResourcesLanguageAttribute +dotnet_analyzer_diagnostic.CA1824.severity = warning + +# CA1825: Avoid zero-length array allocations +dotnet_analyzer_diagnostic.CA1825.severity = warning + +# CA1826: Do not use Enumerable methods on indexable collections +dotnet_analyzer_diagnostic.CA1826.severity = warning + +# CA1827: Do not use Count() or LongCount() when Any() can be used +dotnet_analyzer_diagnostic.CA1827.severity = warning + +# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used +dotnet_analyzer_diagnostic.CA1828.severity = warning + +# CA1829: Use Length/Count property instead of Count() when available +dotnet_analyzer_diagnostic.CA1829.severity = warning + +# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder +dotnet_analyzer_diagnostic.CA1830.severity = warning + +# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_analyzer_diagnostic.CA1831.severity = warning + +# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_analyzer_diagnostic.CA1832.severity = warning + +# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_analyzer_diagnostic.CA1833.severity = warning + +# CA1834: Consider using 'StringBuilder.Append(char)' when applicable +dotnet_analyzer_diagnostic.CA1834.severity = warning + +# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +dotnet_analyzer_diagnostic.CA1835.severity = warning + +# CA1836: Prefer IsEmpty over Count +dotnet_analyzer_diagnostic.CA1836.severity = warning + +# CA1837: Use 'Environment.ProcessId' +dotnet_analyzer_diagnostic.CA1837.severity = warning + +# CA1838: Avoid 'StringBuilder' parameters for P/Invokes +dotnet_analyzer_diagnostic.CA1838.severity = warning + +# CA1839: Use 'Environment.ProcessPath' +dotnet_analyzer_diagnostic.CA1839.severity = warning + +# CA1840: Use 'Environment.CurrentManagedThreadId' +dotnet_analyzer_diagnostic.CA1840.severity = warning + +# CA1841: Prefer Dictionary.Contains methods +dotnet_analyzer_diagnostic.CA1841.severity = warning + +# CA1842: Do not use 'WhenAll' with a single task +dotnet_analyzer_diagnostic.CA1842.severity = warning + +# CA1843: Do not use 'WaitAll' with a single task +dotnet_analyzer_diagnostic.CA1843.severity = warning + +# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' +dotnet_analyzer_diagnostic.CA1844.severity = warning + +# CA1845: Use span-based 'string.Concat' +dotnet_analyzer_diagnostic.CA1845.severity = warning + +# CA1846: Prefer 'AsSpan' over 'Substring' +dotnet_analyzer_diagnostic.CA1846.severity = warning + +# CA1847: Use char literal for a single character lookup +dotnet_analyzer_diagnostic.CA1847.severity = warning + +# CA1848: Use the LoggerMessage delegates +dotnet_analyzer_diagnostic.CA1848.severity = suggestion + +# CA1849: Call async methods when in an async method +dotnet_analyzer_diagnostic.CA1849.severity = warning + +# CA1850: Prefer static 'HashData' method over 'ComputeHash' +dotnet_analyzer_diagnostic.CA1850.severity = warning + +# CA2000: Dispose objects before losing scope +dotnet_analyzer_diagnostic.CA2000.severity = warning + +# CA2002: Do not lock on objects with weak identity +dotnet_analyzer_diagnostic.CA2002.severity = warning + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_analyzer_diagnostic.CA2007.severity = silent + +# CA2008: Do not create tasks without passing a TaskScheduler +dotnet_analyzer_diagnostic.CA2008.severity = warning + +# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value +dotnet_analyzer_diagnostic.CA2009.severity = warning + +# CA2011: Avoid infinite recursion +dotnet_analyzer_diagnostic.CA2011.severity = warning + +# CA2012: Use ValueTasks correctly +dotnet_analyzer_diagnostic.CA2012.severity = warning + +# CA2013: Do not use ReferenceEquals with value types +dotnet_analyzer_diagnostic.CA2013.severity = warning + +# CA2014: Do not use stackalloc in loops +dotnet_analyzer_diagnostic.CA2014.severity = warning + +# CA2015: Do not define finalizers for types derived from MemoryManager +dotnet_analyzer_diagnostic.CA2015.severity = warning + +# CA2016: Forward the 'CancellationToken' parameter to methods +dotnet_analyzer_diagnostic.CA2016.severity = warning + +# CA2017: Parameter count mismatch +dotnet_analyzer_diagnostic.CA2017.severity = warning + +# CA2018: 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument +dotnet_analyzer_diagnostic.CA2018.severity = warning + +# CA2100: Review SQL queries for security vulnerabilities +dotnet_analyzer_diagnostic.CA2100.severity = warning + +# CA2101: Specify marshaling for P/Invoke string arguments +dotnet_analyzer_diagnostic.CA2101.severity = warning + +# CA2109: Review visible event handlers +dotnet_analyzer_diagnostic.CA2109.severity = warning + +# CA2119: Seal methods that satisfy private interfaces +dotnet_analyzer_diagnostic.CA2119.severity = warning + +# CA2153: Do Not Catch Corrupted State Exceptions +dotnet_analyzer_diagnostic.CA2153.severity = warning + +# CA2200: Rethrow to preserve stack details +dotnet_analyzer_diagnostic.CA2200.severity = warning + +# CA2201: Do not raise reserved exception types +dotnet_analyzer_diagnostic.CA2201.severity = warning + +# CA2207: Initialize value type static fields inline +dotnet_analyzer_diagnostic.CA2207.severity = warning + +# CA2208: Instantiate argument exceptions correctly +dotnet_analyzer_diagnostic.CA2208.severity = warning + +# CA2211: Non-constant fields should not be visible +dotnet_analyzer_diagnostic.CA2211.severity = warning + +# CA2213: Disposable fields should be disposed +dotnet_analyzer_diagnostic.CA2213.severity = warning + +# CA2214: Do not call overridable methods in constructors +dotnet_analyzer_diagnostic.CA2214.severity = warning + +# CA2215: Dispose methods should call base class dispose +dotnet_analyzer_diagnostic.CA2215.severity = warning + +# CA2216: Disposable types should declare finalizer +dotnet_analyzer_diagnostic.CA2216.severity = warning + +# CA2217: Do not mark enums with FlagsAttribute +dotnet_analyzer_diagnostic.CA2217.severity = warning + +# CA2218: Override GetHashCode on overriding Equals +dotnet_analyzer_diagnostic.CA2218.severity = warning + +# CA2219: Do not raise exceptions in finally clauses +dotnet_analyzer_diagnostic.CA2219.severity = warning + +# CA2224: Override Equals on overloading operator equals +dotnet_analyzer_diagnostic.CA2224.severity = warning + +# CA2225: Operator overloads have named alternates +dotnet_analyzer_diagnostic.CA2225.severity = warning + +# CA2226: Operators should have symmetrical overloads +dotnet_analyzer_diagnostic.CA2226.severity = warning + +# CA2227: Collection properties should be read only +dotnet_analyzer_diagnostic.CA2227.severity = warning + +# CA2229: Implement serialization constructors +dotnet_analyzer_diagnostic.CA2229.severity = warning + +# CA2231: Overload operator equals on overriding value type Equals +dotnet_analyzer_diagnostic.CA2231.severity = warning + +# CA2234: Pass system uri objects instead of strings +dotnet_analyzer_diagnostic.CA2234.severity = warning + +# CA2235: Mark all non-serializable fields +dotnet_analyzer_diagnostic.CA2235.severity = warning + +# CA2237: Mark ISerializable types with serializable +dotnet_analyzer_diagnostic.CA2237.severity = warning + +# CA2241: Provide correct arguments to formatting methods +dotnet_analyzer_diagnostic.CA2241.severity = warning + +# CA2242: Test for NaN correctly +dotnet_analyzer_diagnostic.CA2242.severity = warning + +# CA2243: Attribute string literals should parse correctly +dotnet_analyzer_diagnostic.CA2243.severity = warning + +# CA2244: Do not duplicate indexed element initializations +dotnet_analyzer_diagnostic.CA2244.severity = warning + +# CA2245: Do not assign a property to itself +dotnet_analyzer_diagnostic.CA2245.severity = warning + +# CA2246: Assigning symbol and its member in the same statement +dotnet_analyzer_diagnostic.CA2246.severity = warning + +# CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum +dotnet_analyzer_diagnostic.CA2247.severity = warning + +# CA2248: Provide correct 'enum' argument to 'Enum.HasFlag' +dotnet_analyzer_diagnostic.CA2248.severity = warning + +# CA2249: Consider using 'string.Contains' instead of 'string.IndexOf' +dotnet_analyzer_diagnostic.CA2249.severity = warning + +# CA2250: Use 'ThrowIfCancellationRequested' +dotnet_analyzer_diagnostic.CA2250.severity = warning + +# CA2251: Use 'string.Equals' +dotnet_analyzer_diagnostic.CA2251.severity = warning + +# CA2252: This API requires opting into preview features +dotnet_analyzer_diagnostic.CA2252.severity = error + +# CA2253: Named placeholders should not be numeric values +dotnet_analyzer_diagnostic.CA2253.severity = warning + +# CA2254: Template should be a static expression +dotnet_analyzer_diagnostic.CA2254.severity = warning + +# CA2255: The 'ModuleInitializer' attribute should not be used in libraries +dotnet_analyzer_diagnostic.CA2255.severity = warning + +# CA2256: All members declared in parent interfaces must have an implementation in all applied DynamicInterfaceCastableImplementation-attributed interfaces +dotnet_analyzer_diagnostic.CA2256.severity = warning + +# CA2257: Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static' +dotnet_analyzer_diagnostic.CA2257.severity = warning + +# CA2258: Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported +dotnet_analyzer_diagnostic.CA2258.severity = warning + +# CA2259: 'ThreadStatic' only affects static fields +dotnet_analyzer_diagnostic.CA2259.severity = warning + +# CA2260: Use correct type parameter +dotnet_analyzer_diagnostic.CA2260.severity = warning + +# CA2261: Do not use ConfigureAwaitOptions.SuppressThrowing with Task +dotnet_analyzer_diagnostic.CA2261.severity = warning + +# CA2300: Do not use insecure deserializer BinaryFormatter +dotnet_analyzer_diagnostic.CA2300.severity = warning + +# CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder +dotnet_analyzer_diagnostic.CA2301.severity = warning + +# CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize +dotnet_analyzer_diagnostic.CA2302.severity = warning + +# CA2305: Do not use insecure deserializer LosFormatter +dotnet_analyzer_diagnostic.CA2305.severity = warning + +# CA2310: Do not use insecure deserializer NetDataContractSerializer +dotnet_analyzer_diagnostic.CA2310.severity = warning + +# CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder +dotnet_analyzer_diagnostic.CA2311.severity = warning + +# CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing +dotnet_analyzer_diagnostic.CA2312.severity = warning + +# CA2315: Do not use insecure deserializer ObjectStateFormatter +dotnet_analyzer_diagnostic.CA2315.severity = warning + +# CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver +dotnet_analyzer_diagnostic.CA2321.severity = warning + +# CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing +dotnet_analyzer_diagnostic.CA2322.severity = warning + +# CA2326: Do not use TypeNameHandling values other than None +dotnet_analyzer_diagnostic.CA2326.severity = warning + +# CA2327: Do not use insecure JsonSerializerSettings +dotnet_analyzer_diagnostic.CA2327.severity = warning + +# CA2328: Ensure that JsonSerializerSettings are secure +dotnet_analyzer_diagnostic.CA2328.severity = warning + +# CA2329: Do not deserialize with JsonSerializer using an insecure configuration +dotnet_analyzer_diagnostic.CA2329.severity = warning + +# CA2330: Ensure that JsonSerializer has a secure configuration when deserializing +dotnet_analyzer_diagnostic.CA2330.severity = warning + +# CA2350: Do not use DataTable.ReadXml() with untrusted data +dotnet_analyzer_diagnostic.CA2350.severity = warning + +# CA2351: Do not use DataSet.ReadXml() with untrusted data +dotnet_analyzer_diagnostic.CA2351.severity = warning + +# CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +dotnet_analyzer_diagnostic.CA2352.severity = warning + +# CA2353: Unsafe DataSet or DataTable in serializable type +dotnet_analyzer_diagnostic.CA2353.severity = warning + +# CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +dotnet_analyzer_diagnostic.CA2354.severity = warning + +# CA2355: Unsafe DataSet or DataTable type found in deserializable object graph +dotnet_analyzer_diagnostic.CA2355.severity = warning + +# CA2356: Unsafe DataSet or DataTable type in web deserializable object graph +dotnet_analyzer_diagnostic.CA2356.severity = warning + +# CA2361: Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data +dotnet_analyzer_diagnostic.CA2361.severity = warning + +# CA2362: Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks +dotnet_analyzer_diagnostic.CA2362.severity = warning + +# CA3001: Review code for SQL injection vulnerabilities +dotnet_analyzer_diagnostic.CA3001.severity = warning + +# CA3002: Review code for XSS vulnerabilities +dotnet_analyzer_diagnostic.CA3002.severity = warning + +# CA3003: Review code for file path injection vulnerabilities +dotnet_analyzer_diagnostic.CA3003.severity = warning + +# CA3004: Review code for information disclosure vulnerabilities +dotnet_analyzer_diagnostic.CA3004.severity = warning + +# CA3005: Review code for LDAP injection vulnerabilities +dotnet_analyzer_diagnostic.CA3005.severity = warning + +# CA3006: Review code for process command injection vulnerabilities +dotnet_analyzer_diagnostic.CA3006.severity = warning + +# CA3007: Review code for open redirect vulnerabilities +dotnet_analyzer_diagnostic.CA3007.severity = warning + +# CA3008: Review code for XPath injection vulnerabilities +dotnet_analyzer_diagnostic.CA3008.severity = warning + +# CA3009: Review code for XML injection vulnerabilities +dotnet_analyzer_diagnostic.CA3009.severity = warning + +# CA3010: Review code for XAML injection vulnerabilities +dotnet_analyzer_diagnostic.CA3010.severity = warning + +# CA3011: Review code for DLL injection vulnerabilities +dotnet_analyzer_diagnostic.CA3011.severity = warning + +# CA3012: Review code for regex injection vulnerabilities +dotnet_analyzer_diagnostic.CA3012.severity = warning + +# CA5350: Do Not Use Weak Cryptographic Algorithms +dotnet_analyzer_diagnostic.CA5350.severity = warning + +# CA5351: Do Not Use Broken Cryptographic Algorithms +dotnet_analyzer_diagnostic.CA5351.severity = warning + +# CA5358: Review cipher mode usage with cryptography experts +dotnet_analyzer_diagnostic.CA5358.severity = warning + +# CA5359: Do Not Disable Certificate Validation +dotnet_analyzer_diagnostic.CA5359.severity = warning + +# CA5360: Do Not Call Dangerous Methods In Deserialization +dotnet_analyzer_diagnostic.CA5360.severity = warning + +# CA5361: Do Not Disable SChannel Use of Strong Crypto +dotnet_analyzer_diagnostic.CA5361.severity = warning + +# CA5362: Potential reference cycle in deserialized object graph +dotnet_analyzer_diagnostic.CA5362.severity = warning + +# CA5363: Do Not Disable Request Validation +dotnet_analyzer_diagnostic.CA5363.severity = warning + +# CA5364: Do Not Use Deprecated Security Protocols +dotnet_analyzer_diagnostic.CA5364.severity = warning + +# CA5365: Do Not Disable HTTP Header Checking +dotnet_analyzer_diagnostic.CA5365.severity = warning + +# CA5366: Use XmlReader For DataSet Read Xml +dotnet_analyzer_diagnostic.CA5366.severity = warning + +# CA5367: Do Not Serialize Types With Pointer Fields +dotnet_analyzer_diagnostic.CA5367.severity = warning + +# CA5368: Set ViewStateUserKey For Classes Derived From Page +dotnet_analyzer_diagnostic.CA5368.severity = warning + +# CA5369: Use XmlReader For Deserialize +dotnet_analyzer_diagnostic.CA5369.severity = warning + +# CA5370: Use XmlReader For Validating Reader +dotnet_analyzer_diagnostic.CA5370.severity = warning + +# CA5371: Use XmlReader For Schema Read +dotnet_analyzer_diagnostic.CA5371.severity = warning + +# CA5372: Use XmlReader For XPathDocument +dotnet_analyzer_diagnostic.CA5372.severity = warning + +# CA5373: Do not use obsolete key derivation function +dotnet_analyzer_diagnostic.CA5373.severity = warning + +# CA5374: Do Not Use XslTransform +dotnet_analyzer_diagnostic.CA5374.severity = warning + +# CA5375: Do Not Use Account Shared Access Signature +dotnet_analyzer_diagnostic.CA5375.severity = warning + +# CA5376: Use SharedAccessProtocol HttpsOnly +dotnet_analyzer_diagnostic.CA5376.severity = warning + +# CA5377: Use Container Level Access Policy +dotnet_analyzer_diagnostic.CA5377.severity = warning + +# CA5378: Do not disable ServicePointManagerSecurityProtocols +dotnet_analyzer_diagnostic.CA5378.severity = warning + +# CA5379: Ensure Key Derivation Function algorithm is sufficiently strong +dotnet_analyzer_diagnostic.CA5379.severity = warning + +# CA5380: Do Not Add Certificates To Root Store +dotnet_analyzer_diagnostic.CA5380.severity = warning + +# CA5381: Ensure Certificates Are Not Added To Root Store +dotnet_analyzer_diagnostic.CA5381.severity = warning + +# CA5382: Use Secure Cookies In ASP.Net Core +dotnet_analyzer_diagnostic.CA5382.severity = warning + +# CA5383: Ensure Use Secure Cookies In ASP.Net Core +dotnet_analyzer_diagnostic.CA5383.severity = warning + +# CA5384: Do Not Use Digital Signature Algorithm (DSA) +dotnet_analyzer_diagnostic.CA5384.severity = warning + +# CA5385: Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size +dotnet_analyzer_diagnostic.CA5385.severity = warning + +# CA5386: Avoid hardcoding SecurityProtocolType value +dotnet_analyzer_diagnostic.CA5386.severity = warning + +# CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +dotnet_analyzer_diagnostic.CA5387.severity = warning + +# CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +dotnet_analyzer_diagnostic.CA5388.severity = warning + +# CA5389: Do Not Add Archive Item's Path To The Target File System Path +dotnet_analyzer_diagnostic.CA5389.severity = warning + +# CA5390: Do not hard-code encryption key +dotnet_analyzer_diagnostic.CA5390.severity = warning + +# CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers +dotnet_analyzer_diagnostic.CA5391.severity = warning + +# CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes +dotnet_analyzer_diagnostic.CA5392.severity = warning + +# CA5393: Do not use unsafe DllImportSearchPath value +dotnet_analyzer_diagnostic.CA5393.severity = warning + +# CA5394: Do not use insecure randomness +dotnet_analyzer_diagnostic.CA5394.severity = warning + +# CA5395: Miss HttpVerb attribute for action methods +dotnet_analyzer_diagnostic.CA5395.severity = warning + +# CA5396: Set HttpOnly to true for HttpCookie +dotnet_analyzer_diagnostic.CA5396.severity = warning + +# CA5397: Do not use deprecated SslProtocols values +dotnet_analyzer_diagnostic.CA5397.severity = warning + +# CA5398: Avoid hardcoded SslProtocols values +dotnet_analyzer_diagnostic.CA5398.severity = warning + +# CA5399: HttpClients should enable certificate revocation list checks +dotnet_analyzer_diagnostic.CA5399.severity = warning + +# CA5400: Ensure HttpClient certificate revocation list check is not disabled +dotnet_analyzer_diagnostic.CA5400.severity = warning + +# CA5401: Do not use CreateEncryptor with non-default IV +dotnet_analyzer_diagnostic.CA5401.severity = warning + +# CA5402: Use CreateEncryptor with the default IV +dotnet_analyzer_diagnostic.CA5402.severity = warning + +# CA5403: Do not hard-code certificate +dotnet_analyzer_diagnostic.CA5403.severity = warning + +# CA5404: Do not disable token validation checks +dotnet_analyzer_diagnostic.CA5404.severity = warning + +# CA5405: Do not always skip token validation in delegates +dotnet_analyzer_diagnostic.CA5405.severity = warning \ No newline at end of file diff --git a/src/dotnet/.gitignore b/src/dotnet/.gitignore new file mode 100644 index 0000000..202b9bc --- /dev/null +++ b/src/dotnet/.gitignore @@ -0,0 +1,5 @@ +# Documentation XML files generated by the compiler +*.xml + +# IDE generated documentation +docs/api/ \ No newline at end of file diff --git a/src/dotnet/Directory.Build.props b/src/dotnet/Directory.Build.props new file mode 100644 index 0000000..5bd3f1b --- /dev/null +++ b/src/dotnet/Directory.Build.props @@ -0,0 +1,35 @@ + + + + true + Recommended + true + true + false + + + README.md + git + + + latest + enable + enable + + IDE1006;IDE0008;IDE0022;IDE0040;IDE0060;IDE0061;IDE0062;CS1591 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + \ No newline at end of file diff --git a/src/dotnet/PloyBus.Tests/PloyBus.Tests.csproj b/src/dotnet/PloyBus.Tests/PloyBus.Tests.csproj new file mode 100644 index 0000000..437024f --- /dev/null +++ b/src/dotnet/PloyBus.Tests/PloyBus.Tests.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + PolyBus + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/dotnet/PloyBus.Tests/PolyBusTests.cs b/src/dotnet/PloyBus.Tests/PolyBusTests.cs new file mode 100644 index 0000000..01131fc --- /dev/null +++ b/src/dotnet/PloyBus.Tests/PolyBusTests.cs @@ -0,0 +1,198 @@ +using NUnit.Framework; +using PolyBus.Transport.Transactions; + +namespace PolyBus; + +[TestFixture] +public class PolyBusTests +{ + [Test] + public async Task IncomingHandlers_IsInvoked() + { + // Arrange + var incomingTransactionTask = new TaskCompletionSource(); + var builder = new PolyBusBuilder + { + IncomingHandlers = + { + async (transaction, next) => + { + await next(); + incomingTransactionTask.SetResult(transaction); + } + } + }; + var bus = await builder.Build(); + + // Act + await bus.Start(); + var outgoingTransaction = await bus.CreateTransaction(); + outgoingTransaction.AddOutgoingMessage("Hello world", "unknown-endpoint"); + await outgoingTransaction.Commit(); + await bus.Start(); + var transaction = await incomingTransactionTask.Task; + await Task.Yield(); + await bus.Stop(); + + //Assert + Assert.That(transaction.IncomingMessage.Body, Is.EqualTo("Hello world")); + } + + [Test] + public async Task IncomingHandlers_WithDelay_IsInvoked() + { + // Arrange + var processedOnTask = new TaskCompletionSource(); + var builder = new PolyBusBuilder + { + IncomingHandlers = + { + async (_, next) => + { + await next(); + processedOnTask.SetResult(DateTime.UtcNow); + } + } + }; + var bus = await builder.Build(); + + // Act + await bus.Start(); + var outgoingTransaction = await bus.CreateTransaction(); + var message = outgoingTransaction.AddOutgoingMessage("Hello world", "unknown-endpoint"); + var scheduledAt = DateTime.UtcNow.AddSeconds(5); + message.DeliverAt = scheduledAt; + await outgoingTransaction.Commit(); + await bus.Start(); + var processedOn = await processedOnTask.Task; + await Task.Yield(); + await bus.Stop(); + + //Assert + Assert.That(processedOn, Is.GreaterThanOrEqualTo(scheduledAt)); + } + + [Test] + public async Task IncomingHandlers_WithDelayAndException_IsInvoked() + { + // Arrange + var processedOnTask = new TaskCompletionSource(); + var builder = new PolyBusBuilder + { + IncomingHandlers = + { + (transaction, next) => + { + processedOnTask.SetResult(DateTime.UtcNow); + throw new Exception(transaction.IncomingMessage.Body); + } + } + }; + var bus = await builder.Build(); + + // Act + await bus.Start(); + var outgoingTransaction = await bus.CreateTransaction(); + var message = outgoingTransaction.AddOutgoingMessage("Hello world", "unknown-endpoint"); + var scheduledAt = DateTime.UtcNow.AddSeconds(5); + message.DeliverAt = scheduledAt; + await outgoingTransaction.Commit(); + await bus.Start(); + var processedOn = await processedOnTask.Task; + await Task.Yield(); + await bus.Stop(); + + //Assert + Assert.That(processedOn.AddSeconds(1), Is.GreaterThanOrEqualTo(scheduledAt)); + } + + [Test] + public async Task IncomingHandlers_WithException_IsInvoked() + { + // Arrange + var incomingTransactionTask = new TaskCompletionSource(); + var builder = new PolyBusBuilder + { + IncomingHandlers = + { + (transaction, _) => + { + incomingTransactionTask.SetResult(transaction); + throw new Exception(transaction.IncomingMessage.Body); + } + } + }; + var bus = await builder.Build(); + + // Act + await bus.Start(); + var outgoingTransaction = await bus.CreateTransaction(); + outgoingTransaction.AddOutgoingMessage("Hello world", "unknown-endpoint"); + await outgoingTransaction.Commit(); + await bus.Start(); + var transaction = await incomingTransactionTask.Task; + await Task.Yield(); + await bus.Stop(); + + //Assert + Assert.That(transaction.IncomingMessage.Body, Is.EqualTo("Hello world")); + } + + [Test] + public async Task OutgoingHandlers_IsInvoked() + { + // Arrange + var outgoingTransactionTask = new TaskCompletionSource(); + var builder = new PolyBusBuilder + { + OutgoingHandlers = + { + async (transaction, next) => + { + await next(); + outgoingTransactionTask.SetResult(transaction); + } + } + }; + var bus = await builder.Build(); + + // Act + await bus.Start(); + var outgoingTransaction = await bus.CreateTransaction(); + outgoingTransaction.AddOutgoingMessage("Hello world", "unknown-endpoint"); + await outgoingTransaction.Commit(); + await bus.Start(); + var transaction = await outgoingTransactionTask.Task; + await Task.Yield(); + await bus.Stop(); + + //Assert + Assert.That(transaction.OutgoingMessages.Count, Is.EqualTo(1)); + Assert.That(transaction.OutgoingMessages[0].Body, Is.EqualTo("Hello world")); + } + + [Test] + public async Task OutgoingHandlers_WithException_IsInvoked() + { + // Arrange + var builder = new PolyBusBuilder + { + OutgoingHandlers = + { + (transaction, _) => throw new Exception(transaction.OutgoingMessages[0].Body) + } + }; + var bus = await builder.Build(); + + // Act + await bus.Start(); + var outgoingTransaction = await bus.CreateTransaction(); + outgoingTransaction.AddOutgoingMessage("Hello world", "unknown-endpoint"); + var ex = Assert.ThrowsAsync(outgoingTransaction.Commit); + await bus.Start(); + await bus.Stop(); + + //Assert + Assert.That(ex.Message, Is.EqualTo("Hello world")); + } +} diff --git a/src/dotnet/PloyBus.Tests/Properties/AssemlyInfo.cs b/src/dotnet/PloyBus.Tests/Properties/AssemlyInfo.cs new file mode 100644 index 0000000..5e80f0d --- /dev/null +++ b/src/dotnet/PloyBus.Tests/Properties/AssemlyInfo.cs @@ -0,0 +1,3 @@ +using System.Diagnostics.CodeAnalysis; + +[assembly: ExcludeFromCodeCoverage] diff --git a/src/dotnet/PloyBus.Tests/Transport/InMemory/InMemoryTests.cs b/src/dotnet/PloyBus.Tests/Transport/InMemory/InMemoryTests.cs new file mode 100644 index 0000000..f323b26 --- /dev/null +++ b/src/dotnet/PloyBus.Tests/Transport/InMemory/InMemoryTests.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using NUnit.Framework; +using PolyBus.Transport.Transactions; +using PolyBus.Transport.Transactions.Messages; + +namespace PolyBus.Transport.InMemory; + +[TestFixture] +public class InMemoryTests +{ + private readonly MessageInfo _messageInfo = typeof(TestMessage).GetCustomAttribute()!; + + [Test] + public async Task InMemory_WithSubscription() + { + // Arrange + var inMemoryTransport = new InMemoryTransport + { + UseSubscriptions = true + }; + var incomingTransactionTask = new TaskCompletionSource(); + var builder = new PolyBusBuilder + { + IncomingHandlers = + { + async (transaction, next) => + { + incomingTransactionTask.SetResult(transaction); + await next(); + } + }, + TransportFactory = (builder, bus) => Task.FromResult(inMemoryTransport.AddEndpoint(builder, bus)) + }; + builder.Messages.Add(typeof(TestMessage)); + var bus = await builder.Build(); + await bus.Transport.Subscribe(typeof(TestMessage).GetCustomAttribute()!); + + // Act + await bus.Start(); + var outgoingTransaction = await bus.CreateTransaction(); + var outgoingMessage = outgoingTransaction.AddOutgoingMessage(new TestMessage + { + Name = "TestMessage" + }); + outgoingMessage.Headers[Headers.MessageType] = _messageInfo.ToString(true); + await outgoingTransaction.Commit(); + await bus.Start(); + var incomingTransaction = await incomingTransactionTask.Task; + await Task.Yield(); + await bus.Stop(); + + //Assert + Assert.That(incomingTransaction.IncomingMessage.Body, Is.EqualTo("TestMessage")); + Assert.That(incomingTransaction.IncomingMessage.Headers[Headers.MessageType], Is.EqualTo(_messageInfo.ToString(true))); + } + + [MessageInfo(MessageType.Command, "test-service", "TestMessage", 1, 0, 0)] + public class TestMessage + { + public override string ToString() => Name; + public required string Name { get; init; } = string.Empty; + } +} diff --git a/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/Handlers/Error/ErrorHandlerTests.cs b/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/Handlers/Error/ErrorHandlerTests.cs new file mode 100644 index 0000000..9811194 --- /dev/null +++ b/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/Handlers/Error/ErrorHandlerTests.cs @@ -0,0 +1,399 @@ +using NUnit.Framework; + +namespace PolyBus.Transport.Transactions.Messages.Handlers.Error; + +[TestFixture] +public class ErrorHandlerTests +{ + private TestBus _testBus = null!; + private IncomingMessage _incomingMessage = null!; + private IncomingTransaction _transaction = null!; + private TestableErrorHandler _errorHandler = null!; + + [SetUp] + public void SetUp() + { + _testBus = new TestBus("TestBus"); + _incomingMessage = new IncomingMessage(_testBus, "test message body"); + _transaction = new IncomingTransaction(_testBus, _incomingMessage); + _errorHandler = new TestableErrorHandler(); + } + + [Test] + public async Task Retrier_SucceedsOnFirstAttempt_DoesNotRetry() + { + // Arrange + var nextCalled = false; + + Task Next() + { + nextCalled = true; + return Task.CompletedTask; + } + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(0)); + } + + [Test] + public async Task Retrier_FailsOnce_RetriesImmediately() + { + // Arrange + var callCount = 0; + + Task Next() + { + callCount++; + if (callCount == 1) + { + throw new Exception("Test error"); + } + + return Task.CompletedTask; + } + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(callCount, Is.EqualTo(2)); + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(0)); + } + + [Test] + public async Task Retrier_FailsAllImmediateRetries_SchedulesDelayedRetry() + { + // Arrange + var expectedRetryTime = DateTime.UtcNow.AddMinutes(5); + _errorHandler.SetNextRetryTime(expectedRetryTime); + + var callCount = 0; + + Task Next() + { + callCount++; + throw new Exception("Test error"); + } + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(callCount, Is.EqualTo(_errorHandler.ImmediateRetryCount)); + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + + var delayedMessage = _transaction.OutgoingMessages[0]; + Assert.That(delayedMessage.DeliverAt, Is.EqualTo(expectedRetryTime)); + Assert.That(delayedMessage.Headers[ErrorHandler.RetryCountHeader], Is.EqualTo("1")); + Assert.That(delayedMessage.Endpoint, Is.EqualTo("TestBus")); + } + + [Test] + public async Task Retrier_WithExistingRetryCount_IncrementsCorrectly() + { + // Arrange + _incomingMessage.Headers[ErrorHandler.RetryCountHeader] = "2"; + var expectedRetryTime = DateTime.UtcNow.AddMinutes(10); + _errorHandler.SetNextRetryTime(expectedRetryTime); + + Task Next() => throw new Exception("Test error"); + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + + var delayedMessage = _transaction.OutgoingMessages[0]; + Assert.That(delayedMessage.Headers[ErrorHandler.RetryCountHeader], Is.EqualTo("3")); + Assert.That(delayedMessage.DeliverAt, Is.EqualTo(expectedRetryTime)); + } + + [Test] + public async Task Retrier_ExceedsMaxDelayedRetries_SendsToDeadLetter() + { + // Arrange + _incomingMessage.Headers[ErrorHandler.RetryCountHeader] = + _errorHandler.DelayedRetryCount.ToString(); + + var testException = new Exception("Final error"); + + Task Next() => throw testException; + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + + var deadLetterMessage = _transaction.OutgoingMessages[0]; + Assert.That(deadLetterMessage.Endpoint, Is.EqualTo("TestBus.Errors")); + Assert.That(deadLetterMessage.Headers[ErrorHandler.ErrorMessageHeader], Is.EqualTo("Final error")); + Assert.That(deadLetterMessage.Headers[ErrorHandler.ErrorStackTraceHeader], Is.Not.Null); + } + + [Test] + public async Task Retrier_WithCustomDeadLetterEndpoint_UsesCustomEndpoint() + { + // Arrange + _errorHandler = new TestableErrorHandler + { + DeadLetterEndpoint = "CustomDeadLetter" + }; + + _incomingMessage.Headers[ErrorHandler.RetryCountHeader] = + _errorHandler.DelayedRetryCount.ToString(); + + Task Next() => throw new Exception("Final error"); + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + + var deadLetterMessage = _transaction.OutgoingMessages[0]; + Assert.That(deadLetterMessage.Endpoint, Is.EqualTo("CustomDeadLetter")); + } + + [Test] + public async Task Retrier_ClearsOutgoingMessagesOnEachRetry() + { + // Arrange + var callCount = 0; + + Task Next() + { + callCount++; + _transaction.AddOutgoingMessage("some message", "some endpoint"); + throw new Exception("Test error"); + } + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(callCount, Is.EqualTo(_errorHandler.ImmediateRetryCount)); + // Should only have the delayed retry message, not the messages added in next() + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + Assert.That(_transaction.OutgoingMessages[0].Headers.ContainsKey(ErrorHandler.RetryCountHeader), Is.True); + } + + [Test] + public async Task Retrier_WithZeroImmediateRetries_SchedulesDelayedRetryImmediately() + { + // Arrange + _errorHandler = new TestableErrorHandler { ImmediateRetryCount = 0 }; + var expectedRetryTime = DateTime.UtcNow.AddMinutes(5); + _errorHandler.SetNextRetryTime(expectedRetryTime); + + var callCount = 0; + + Task Next() + { + callCount++; + throw new Exception("Test error"); + } + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(callCount, Is.EqualTo(1)); // Should enforce minimum of 1 + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + Assert.That(_transaction.OutgoingMessages[0].Headers[ErrorHandler.RetryCountHeader], Is.EqualTo("1")); + } + + [Test] + public async Task Retrier_WithZeroDelayedRetries_StillGetsMinimumOfOne() + { + // Arrange + _errorHandler = new TestableErrorHandler { DelayedRetryCount = 0 }; + var expectedRetryTime = DateTime.UtcNow.AddMinutes(5); + _errorHandler.SetNextRetryTime(expectedRetryTime); + + Task Next() => throw new Exception("Test error"); + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + // Even with DelayedRetryCount = 0, Math.Max(1, DelayedRetryCount) makes it 1 + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + Assert.That(_transaction.OutgoingMessages[0].Headers[ErrorHandler.RetryCountHeader], Is.EqualTo("1")); + Assert.That(_transaction.OutgoingMessages[0].DeliverAt, Is.EqualTo(expectedRetryTime)); + } + + [Test] + public void GetNextRetryTime_DefaultImplementation_UsesDelayCorrectly() + { + // Arrange + var handler = new ErrorHandler { Delay = 60 }; + var beforeTime = DateTime.UtcNow; + + // Act + var result1 = handler.GetNextRetryTime(1); + var result2 = handler.GetNextRetryTime(2); + var result3 = handler.GetNextRetryTime(3); + + var afterTime = DateTime.UtcNow; + + // Assert + Assert.That(result1, Is.GreaterThanOrEqualTo(beforeTime.AddSeconds(60))); + Assert.That(result1, Is.LessThanOrEqualTo(afterTime.AddSeconds(60))); + + Assert.That(result2, Is.GreaterThanOrEqualTo(beforeTime.AddSeconds(120))); + Assert.That(result2, Is.LessThanOrEqualTo(afterTime.AddSeconds(120))); + + Assert.That(result3, Is.GreaterThanOrEqualTo(beforeTime.AddSeconds(180))); + Assert.That(result3, Is.LessThanOrEqualTo(afterTime.AddSeconds(180))); + } + + [Test] + public async Task Retrier_SucceedsAfterSomeImmediateRetries_StopsRetrying() + { + // Arrange + var callCount = 0; + + Task Next() + { + callCount++; + if (callCount < 3) // Fail first 2 attempts + { + throw new Exception("Test error"); + } + + return Task.CompletedTask; + } + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(callCount, Is.EqualTo(3)); + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(0)); + } + + [Test] + public async Task Retrier_InvalidRetryCountHeader_TreatsAsZero() + { + // Arrange + _incomingMessage.Headers[ErrorHandler.RetryCountHeader] = "invalid"; + var expectedRetryTime = DateTime.UtcNow.AddMinutes(5); + _errorHandler.SetNextRetryTime(expectedRetryTime); + + Task Next() => throw new Exception("Test error"); + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + var delayedMessage = _transaction.OutgoingMessages[0]; + Assert.That(delayedMessage.Headers[ErrorHandler.RetryCountHeader], Is.EqualTo("1")); + } + + [Test] + public async Task Retrier_ExceptionStackTrace_IsStoredInHeader() + { + // Arrange + _incomingMessage.Headers[ErrorHandler.RetryCountHeader] = + _errorHandler.DelayedRetryCount.ToString(); + + var exceptionWithStackTrace = new Exception("Error with stack trace"); + + Task Next() => throw exceptionWithStackTrace; + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + var deadLetterMessage = _transaction.OutgoingMessages[0]; + Assert.That(deadLetterMessage.Headers[ErrorHandler.ErrorStackTraceHeader], Is.Not.Null); + Assert.That(deadLetterMessage.Headers[ErrorHandler.ErrorStackTraceHeader], Is.Not.Empty); + } + + [Test] + public async Task Retrier_ExceptionWithNullStackTrace_UsesEmptyString() + { + // Arrange + _incomingMessage.Headers[ErrorHandler.RetryCountHeader] = + _errorHandler.DelayedRetryCount.ToString(); + + // Create an exception with null StackTrace using custom exception + var exceptionWithoutStackTrace = new ExceptionWithNullStackTrace("Error without stack trace"); + + Task Next() => throw exceptionWithoutStackTrace; + + // Act + await _errorHandler.Retrier(_transaction, Next); + + // Assert + Assert.That(_transaction.OutgoingMessages.Count, Is.EqualTo(1)); + var deadLetterMessage = _transaction.OutgoingMessages[0]; + Assert.That(deadLetterMessage.Headers[ErrorHandler.ErrorStackTraceHeader], Is.EqualTo(string.Empty)); + } + + // Helper class to override GetNextRetryTime for testing + private class TestableErrorHandler : ErrorHandler + { + private DateTime? _nextRetryTime; + + public void SetNextRetryTime(DateTime retryTime) + { + _nextRetryTime = retryTime; + } + + public override DateTime GetNextRetryTime(int attempt) + { + return _nextRetryTime ?? base.GetNextRetryTime(attempt); + } + } + + // Custom exception that returns null for StackTrace + private class ExceptionWithNullStackTrace(string message) : Exception(message) + { + public override string? StackTrace => null; + } + + // Test implementation of IPolyBus for testing purposes + private class TestBus(string name) : IPolyBus + { + public IDictionary Properties { get; } = new Dictionary(); + public ITransport Transport { get; } = new TestTransport(); + public IList IncomingHandlers { get; } = []; + public IList OutgoingHandlers { get; } = []; + public Messages Messages { get; } = new(); + public string Name { get; } = name; + + public Task CreateTransaction(IncomingMessage? message = null) + { + Transaction transaction = message == null + ? new OutgoingTransaction(this) + : new IncomingTransaction(this, message); + return Task.FromResult(transaction); + } + + public Task Send(Transaction transaction) => Task.CompletedTask; + public Task Start() => Task.CompletedTask; + public Task Stop() => Task.CompletedTask; + } + + // Simple test transport implementation + private class TestTransport : ITransport + { + public bool SupportsCommandMessages => true; + public bool SupportsDelayedMessages => true; + public bool SupportsSubscriptions => false; + + public Task Send(Transaction transaction) => Task.CompletedTask; + public Task Subscribe(MessageInfo messageInfo) => Task.CompletedTask; + public Task Start() => Task.CompletedTask; + public Task Stop() => Task.CompletedTask; + } +} diff --git a/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/Handlers/Serializers/JsonHandlersTests.cs b/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/Handlers/Serializers/JsonHandlersTests.cs new file mode 100644 index 0000000..751039c --- /dev/null +++ b/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/Handlers/Serializers/JsonHandlersTests.cs @@ -0,0 +1,425 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using NUnit.Framework; + +namespace PolyBus.Transport.Transactions.Messages.Handlers.Serializers; + +[TestFixture] +public class JsonHandlersTests +{ + private JsonHandlers _jsonHandlers = null!; + private IPolyBus _mockBus = null!; + private Messages _messages = null!; + + [SetUp] + public void SetUp() + { + _jsonHandlers = new JsonHandlers(); + _messages = new Messages(); + _mockBus = new MockPolyBus(_messages); + } + + #region Deserializer Tests + + [Test] + public async Task Deserializer_WithValidTypeHeader_DeserializesMessage() + { + // Arrange + var testMessage = new TestMessage { Id = 1, Name = "Test" }; + var serializedBody = JsonSerializer.Serialize(testMessage); + + _messages.Add(typeof(TestMessage)); + + var incomingMessage = new IncomingMessage(_mockBus, serializedBody) + { + Headers = new Dictionary + { + [Headers.MessageType] = _header + } + }; + var transaction = new MockIncomingTransaction(_mockBus, incomingMessage); + + var nextCalled = false; + Task Next() { nextCalled = true; return Task.CompletedTask; } + + // Act + await _jsonHandlers.Deserializer(transaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + Assert.That(incomingMessage.Message, Is.Not.Null); + Assert.That(incomingMessage.Message, Is.TypeOf()); + var deserializedMessage = (TestMessage)incomingMessage.Message; + Assert.That(deserializedMessage.Id, Is.EqualTo(1)); + Assert.That(deserializedMessage.Name, Is.EqualTo("Test")); + } + + [Test] + public async Task Deserializer_WithValidTypeHeaderAndCustomOptions_DeserializesWithOptions() + { + // Arrange + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + var jsonHandlers = new JsonHandlers { JsonSerializerOptions = options }; + + var testMessage = new TestMessage { Id = 2, Name = "CamelCase" }; + var serializedBody = JsonSerializer.Serialize(testMessage, options); + + _messages.Add(typeof(TestMessage)); + + var incomingMessage = new IncomingMessage(_mockBus, serializedBody) + { + Headers = new Dictionary + { + [Headers.MessageType] = _header + } + }; + var transaction = new MockIncomingTransaction(_mockBus, incomingMessage); + + var nextCalled = false; + Task Next() { nextCalled = true; return Task.CompletedTask; } + + // Act + await jsonHandlers.Deserializer(transaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + Assert.That(incomingMessage.Message, Is.TypeOf()); + var deserializedMessage = (TestMessage)incomingMessage.Message; + Assert.That(deserializedMessage.Id, Is.EqualTo(2)); + Assert.That(deserializedMessage.Name, Is.EqualTo("CamelCase")); + } + + [Test] + public async Task Deserializer_WithUnknownTypeAndThrowOnMissingTypeFalse_ParsesAsJsonNode() + { + // Arrange + var jsonHandlers = new JsonHandlers { ThrowOnMissingType = false }; + var testObject = new { Id = 3, Name = "Unknown" }; + var serializedBody = JsonSerializer.Serialize(testObject); + var header = "endpoint=test-service, type=Command, name=UnknownMessage, version=1.0.0"; + + var incomingMessage = new IncomingMessage(_mockBus, serializedBody) + { + Headers = new Dictionary + { + [Headers.MessageType] = header + } + }; + var transaction = new MockIncomingTransaction(_mockBus, incomingMessage); + + var nextCalled = false; + Task Next() { nextCalled = true; return Task.CompletedTask; } + + // Act + await jsonHandlers.Deserializer(transaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + Assert.That(incomingMessage.Message, Is.Not.Null); + Assert.That(incomingMessage.Message, Is.InstanceOf()); + var jsonNode = (JsonNode)incomingMessage.Message; + Assert.That(jsonNode["Id"]?.GetValue(), Is.EqualTo(3)); + Assert.That(jsonNode["Name"]?.GetValue(), Is.EqualTo("Unknown")); + } + + [Test] + public void Deserializer_WithUnknownTypeAndThrowOnMissingTypeTrue_ThrowsException() + { + // Arrange + var jsonHandlers = new JsonHandlers { ThrowOnMissingType = true }; + var testObject = new { Id = 4, Name = "Error" }; + var serializedBody = JsonSerializer.Serialize(testObject); + var header = "endpoint=test-service, type=Command, name=UnknownMessage, version=1.0.0"; + + var incomingMessage = new IncomingMessage(_mockBus, serializedBody) + { + Headers = new Dictionary + { + [Headers.MessageType] = header + } + }; + var transaction = new MockIncomingTransaction(_mockBus, incomingMessage); + + Task Next() => Task.CompletedTask; + + // Act & Assert + var ex = Assert.ThrowsAsync( + () => jsonHandlers.Deserializer(transaction, Next)); + Assert.That(ex!.Message, Is.EqualTo("The type header is missing, invalid, or if the type cannot be found.")); + } + + [Test] + public void Deserializer_WithMissingTypeHeader_SkipsDeserialization() + { + // Arrange + var jsonHandlers = new JsonHandlers { ThrowOnMissingType = true }; + var incomingMessage = new IncomingMessage(_mockBus, "{}") + { + Headers = new Dictionary() + }; + var transaction = new MockIncomingTransaction(_mockBus, incomingMessage); + + Task Next() => Task.CompletedTask; + + // Act & Assert + var ex = Assert.ThrowsAsync( + () => jsonHandlers.Deserializer(transaction, Next)); + Assert.That(ex!.Message, Is.EqualTo("The type header is missing, invalid, or if the type cannot be found.")); + } + + [Test] + public void Deserializer_WithInvalidJson_ThrowsJsonException() + { + // Arrange + _messages.Add(typeof(TestMessage)); + + var incomingMessage = new IncomingMessage(_mockBus, "invalid json") + { + Headers = new Dictionary + { + [Headers.MessageType] = _header + } + }; + var transaction = new MockIncomingTransaction(_mockBus, incomingMessage); + + Task Next() => Task.CompletedTask; + + // Act & Assert + Assert.ThrowsAsync(() => _jsonHandlers.Deserializer(transaction, Next)); + } + + #endregion + + #region Serializer Tests + + [Test] + public async Task Serializer_WithValidMessage_SerializesAndSetsHeaders() + { + // Arrange + var testMessage = new TestMessage { Id = 5, Name = "Serialize" }; + var messageType = typeof(TestMessage); + var expectedHeader = _header; + + _messages.Add(messageType); + + var mockTransaction = new MockOutgoingTransaction(_mockBus); + var outgoingMessage = mockTransaction.AddOutgoingMessage(testMessage); + + var nextCalled = false; + Task Next() { nextCalled = true; return Task.CompletedTask; } + + // Act + await _jsonHandlers.Serializer(mockTransaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + Assert.That(outgoingMessage.Body, Is.Not.Null); + + var deserializedMessage = JsonSerializer.Deserialize(outgoingMessage.Body); + Assert.That(deserializedMessage!.Id, Is.EqualTo(5)); + Assert.That(deserializedMessage.Name, Is.EqualTo("Serialize")); + + Assert.That(outgoingMessage.Headers[Headers.ContentType], Is.EqualTo("application/json")); + Assert.That(outgoingMessage.Headers[Headers.MessageType], Is.EqualTo(expectedHeader)); + } + + [Test] + public async Task Serializer_WithCustomContentType_UsesCustomContentType() + { + // Arrange + var customContentType = "application/custom-json"; + var jsonHandlers = new JsonHandlers { ContentType = customContentType, ThrowOnInvalidType = false }; + + var testMessage = new TestMessage { Id = 6, Name = "Custom" }; + _messages.Add(typeof(TestMessage)); + + var mockTransaction = new MockOutgoingTransaction(_mockBus); + var outgoingMessage = mockTransaction.AddOutgoingMessage(testMessage); + + var nextCalled = false; + Task Next() { nextCalled = true; return Task.CompletedTask; } + + // Act + await jsonHandlers.Serializer(mockTransaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + Assert.That(outgoingMessage.Headers[Headers.ContentType], Is.EqualTo(customContentType)); + } + + [Test] + public async Task Serializer_WithCustomJsonOptions_SerializesWithOptions() + { + // Arrange + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + var jsonHandlers = new JsonHandlers { JsonSerializerOptions = options, ThrowOnInvalidType = false }; + + var testMessage = new TestMessage { Id = 7, Name = "Options" }; + _messages.Add(typeof(TestMessage)); + + var mockTransaction = new MockOutgoingTransaction(_mockBus); + var outgoingMessage = mockTransaction.AddOutgoingMessage(testMessage); + + var nextCalled = false; + Task Next() { nextCalled = true; return Task.CompletedTask; } + + // Act + await jsonHandlers.Serializer(mockTransaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + Assert.That(outgoingMessage.Body, Contains.Substring("\"id\":")); + Assert.That(outgoingMessage.Body, Contains.Substring("\"name\":")); + } + + [Test] + public async Task Serializer_WithUnknownTypeAndThrowOnInvalidTypeFalse_SkipsHeaderSetting() + { + // Arrange + var jsonHandlers = new JsonHandlers { ThrowOnInvalidType = false }; + + var testMessage = new UnknownMessage { Data = "test" }; + + var mockTransaction = new MockOutgoingTransaction(_mockBus); + var outgoingMessage = mockTransaction.AddOutgoingMessage(testMessage, "unknown-endpoint"); + + var nextCalled = false; + Task Next() { nextCalled = true; return Task.CompletedTask; } + + // Act + await jsonHandlers.Serializer(mockTransaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + Assert.That(outgoingMessage.Body, Is.Not.Null); + Assert.That(outgoingMessage.Headers[Headers.ContentType], Is.EqualTo("application/json")); + Assert.That(outgoingMessage.Headers.ContainsKey(Headers.MessageType), Is.False); + } + + [Test] + public void Serializer_WithUnknownTypeAndThrowOnInvalidTypeTrue_ThrowsException() + { + // Arrange + var jsonHandlers = new JsonHandlers { ThrowOnInvalidType = true }; + + var testMessage = new UnknownMessage { Data = "error" }; + + var mockTransaction = new MockOutgoingTransaction(_mockBus); + mockTransaction.AddOutgoingMessage(testMessage, "unknown-endpoint"); + + Task Next() => Task.CompletedTask; + + // Act & Assert + var ex = Assert.ThrowsAsync( + () => jsonHandlers.Serializer(mockTransaction, Next)); + Assert.That(ex!.Message, Is.EqualTo("The header has an valid type.")); + } + + [Test] + public async Task Serializer_WithMultipleMessages_SerializesAll() + { + // Arrange + var testMessage1 = new TestMessage { Id = 8, Name = "First" }; + var testMessage2 = new TestMessage { Id = 9, Name = "Second" }; + + _messages.Add(typeof(TestMessage)); + + var mockTransaction = new MockOutgoingTransaction(_mockBus); + var outgoingMessage1 = mockTransaction.AddOutgoingMessage(testMessage1); + outgoingMessage1.Headers[Headers.MessageType] = _header; + var outgoingMessage2 = mockTransaction.AddOutgoingMessage(testMessage2); + outgoingMessage2.Headers[Headers.MessageType] = _header; + + var nextCalled = false; + Task Next() { nextCalled = true; return Task.CompletedTask; } + + // Act + await _jsonHandlers.Serializer(mockTransaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + Assert.That(outgoingMessage1.Body, Is.Not.Null); + Assert.That(outgoingMessage2.Body, Is.Not.Null); + + var deserializedMessage1 = JsonSerializer.Deserialize(outgoingMessage1.Body); + var deserializedMessage2 = JsonSerializer.Deserialize(outgoingMessage2.Body); + + Assert.That(deserializedMessage1!.Id, Is.EqualTo(8)); + Assert.That(deserializedMessage1.Name, Is.EqualTo("First")); + Assert.That(deserializedMessage2!.Id, Is.EqualTo(9)); + Assert.That(deserializedMessage2.Name, Is.EqualTo("Second")); + } + + [Test] + public async Task Serializer_WithEmptyOutgoingMessages_CallsNext() + { + // Arrange + var mockTransaction = new MockOutgoingTransaction(_mockBus); + + var nextCalled = false; + Task Next() { nextCalled = true; return Task.CompletedTask; } + + // Act + await _jsonHandlers.Serializer(mockTransaction, Next); + + // Assert + Assert.That(nextCalled, Is.True); + } + + #endregion + + #region Test Support Classes + + const string _header = "endpoint=test-service, type=Command, name=TestMessage, version=1.0.0"; + [MessageInfo(MessageType.Command, "test-service", "TestMessage", 1, 0, 0)] + public class TestMessage + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } + + public class UnknownMessage + { + public string Data { get; set; } = string.Empty; + } + + #endregion + + private class MockPolyBus(Messages messages) : IPolyBus + { + public IDictionary Properties => null!; + public ITransport Transport => null!; + public IList IncomingHandlers => []; + public IList OutgoingHandlers => []; + public Messages Messages { get; } = messages; + public string Name => "MockBus"; + + public Task CreateTransaction(IncomingMessage? message = null) => + Task.FromResult( + message != null + ? new MockIncomingTransaction(this, message) + : new MockOutgoingTransaction(this)); + + public Task Send(Transaction transaction) => Task.CompletedTask; + public Task Start() => Task.CompletedTask; + public Task Stop() => Task.CompletedTask; + } + + private class MockOutgoingTransaction(IPolyBus bus) : OutgoingTransaction(bus) + { + public override Task Abort() => Task.CompletedTask; + public override Task Commit() => Task.CompletedTask; + } + + private class MockIncomingTransaction(IPolyBus bus, IncomingMessage incomingMessage) : IncomingTransaction(bus, incomingMessage) + { + public override Task Abort() => Task.CompletedTask; + public override Task Commit() => Task.CompletedTask; + } +} diff --git a/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/MessageInfoTests.cs b/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/MessageInfoTests.cs new file mode 100644 index 0000000..fee88d7 --- /dev/null +++ b/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/MessageInfoTests.cs @@ -0,0 +1,263 @@ +using NUnit.Framework; + +// ReSharper disable SuspiciousTypeConversion.Global + +namespace PolyBus.Transport.Transactions.Messages; + +[TestFixture] +public class MessageInfoTests +{ + [Test] + public void GetAttributeFromHeader_WithValidHeader_ReturnsCorrectAttribute() + { + // Arrange + var header = "endpoint=user-service, type=Command, name=CreateUser, version=1.2.3"; + + // Act + var result = MessageInfo.GetAttributeFromHeader(header); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Endpoint, Is.EqualTo("user-service")); + Assert.That(result.Type, Is.EqualTo(MessageType.Command)); + Assert.That(result.Name, Is.EqualTo("CreateUser")); + Assert.That(result.Major, Is.EqualTo(1)); + Assert.That(result.Minor, Is.EqualTo(2)); + Assert.That(result.Patch, Is.EqualTo(3)); + } + + [Test] + public void GetAttributeFromHeader_WithEventType_ReturnsCorrectAttribute() + { + // Arrange + var header = "endpoint=notification-service, type=Event, name=UserCreated, version=2.0.1"; + + // Act + var result = MessageInfo.GetAttributeFromHeader(header); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result!.Endpoint, Is.EqualTo("notification-service")); + Assert.That(result.Type, Is.EqualTo(MessageType.Event)); + Assert.That(result.Name, Is.EqualTo("UserCreated")); + Assert.That(result.Major, Is.EqualTo(2)); + Assert.That(result.Minor, Is.EqualTo(0)); + Assert.That(result.Patch, Is.EqualTo(1)); + } + + [Test] + public void GetAttributeFromHeader_WithExtraSpaces_ReturnsCorrectAttribute() + { + // Arrange - the current regex doesn't handle spaces within values well, so testing valid spacing + var header = "endpoint=payment-service, type=Command, name=ProcessPayment, version=3.14.159"; + + // Act + var result = MessageInfo.GetAttributeFromHeader(header); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result!.Endpoint, Is.EqualTo("payment-service")); + Assert.That(result.Type, Is.EqualTo(MessageType.Command)); + Assert.That(result.Name, Is.EqualTo("ProcessPayment")); + Assert.That(result.Major, Is.EqualTo(3)); + Assert.That(result.Minor, Is.EqualTo(14)); + Assert.That(result.Patch, Is.EqualTo(159)); + } + + [Test] + public void GetAttributeFromHeader_WithCaseInsensitiveType_ReturnsCorrectAttribute() + { + // Arrange + var header = "endpoint=order-service, type=command, name=PlaceOrder, version=1.0.0"; + + // Act + var result = MessageInfo.GetAttributeFromHeader(header); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result!.Type, Is.EqualTo(MessageType.Command)); + } + + [TestCase("")] + [TestCase("invalid header")] + [TestCase("endpoint=test")] + [TestCase("endpoint=test, type=Command")] + [TestCase("endpoint=test, type=Command, name=Test")] + [TestCase("endpoint=test, type=Command, name=Test, version=invalid")] + [TestCase("type=Command, name=Test, version=1.0.0")] + public void GetAttributeFromHeader_WithInvalidHeader_ReturnsNull(string header) + { + // Act + var result = MessageInfo.GetAttributeFromHeader(header); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void GetAttributeFromHeader_WithInvalidEnumType_ThrowsArgumentException() + { + // Arrange + var header = "endpoint=test, type=InvalidType, name=Test, version=1.0.0"; + + // Act & Assert + Assert.Throws(() => MessageInfo.GetAttributeFromHeader(header)); + } + + [Test] + public void GetAttributeFromHeader_WithMissingVersion_ReturnsNull() + { + // Arrange + var header = "endpoint=test-service, type=Command, name=TestCommand, version="; + + // Act + var result = MessageInfo.GetAttributeFromHeader(header); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void GetAttributeFromHeader_WithIncompleteVersion_ReturnsNull() + { + // Arrange + var header = "endpoint=test-service, type=Command, name=TestCommand, version=1.0"; + + // Act + var result = MessageInfo.GetAttributeFromHeader(header); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void Equals_WithIdenticalAttributes_ReturnsTrue() + { + // Arrange + var attr1 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var attr2 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + + // Act & Assert + Assert.That(attr1.Equals(attr2), Is.True); + Assert.That(attr2.Equals(attr1), Is.True); + } + + [Test] + public void Equals_WithSameObject_ReturnsTrue() + { + // Arrange + var attr = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + + // Act & Assert + Assert.That(attr.Equals(attr), Is.True); + } + + [Test] + public void Equals_WithDifferentType_ReturnsFalse() + { + // Arrange + var attr1 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var attr2 = new MessageInfo(MessageType.Event, "user-service", "CreateUser", 1, 2, 3); + + // Act & Assert + Assert.That(attr1.Equals(attr2), Is.False); + } + + [Test] + public void Equals_WithDifferentEndpoint_ReturnsFalse() + { + // Arrange + var attr1 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var attr2 = new MessageInfo(MessageType.Command, "order-service", "CreateUser", 1, 2, 3); + + // Act & Assert + Assert.That(attr1.Equals(attr2), Is.False); + } + + [Test] + public void Equals_WithDifferentName_ReturnsFalse() + { + // Arrange + var attr1 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var attr2 = new MessageInfo(MessageType.Command, "user-service", "UpdateUser", 1, 2, 3); + + // Act & Assert + Assert.That(attr1.Equals(attr2), Is.False); + } + + [Test] + public void Equals_WithDifferentMajorVersion_ReturnsFalse() + { + // Arrange + var attr1 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var attr2 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 2, 2, 3); + + // Act & Assert + Assert.That(attr1.Equals(attr2), Is.False); + } + + [Test] + public void Equals_WithDifferentMinorVersion_ReturnsTrue() + { + // Arrange + var attr1 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var attr2 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 3, 3); + + // Act & Assert + Assert.That(attr1.Equals(attr2), Is.True, "Minor version differences should not affect equality"); + } + + [Test] + public void Equals_WithDifferentPatchVersion_ReturnsTrue() + { + // Arrange + var attr1 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var attr2 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 4); + + // Act & Assert + Assert.That(attr1.Equals(attr2), Is.True, "Patch version differences should not affect equality"); + } + + [Test] + public void Equals_WithNullObject_ReturnsFalse() + { + // Arrange + var attr = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + + // Act & Assert + Assert.That(attr.Equals(null), Is.False); + } + + [Test] + public void Equals_WithDifferentObjectType_ReturnsFalse() + { + // Arrange + var attr = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var other = "not a MessageAttribute"; + + // Act & Assert + Assert.That(attr.Equals(other), Is.False); + } + + [Test] + public void GetHashCode_WithIdenticalAttributes_ReturnsSameHashCode() + { + // Arrange + var attr1 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var attr2 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + + // Act & Assert + Assert.That(attr1.GetHashCode(), Is.EqualTo(attr2.GetHashCode())); + } + + [Test] + public void GetHashCode_WithDifferentAttributes_ReturnsDifferentHashCodes() + { + // Arrange + var attr1 = new MessageInfo(MessageType.Command, "user-service", "CreateUser", 1, 2, 3); + var attr2 = new MessageInfo(MessageType.Event, "user-service", "CreateUser", 1, 2, 3); + + // Act & Assert + Assert.That(attr1.GetHashCode(), Is.Not.EqualTo(attr2.GetHashCode())); + } +} diff --git a/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/MessagesTests.cs b/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/MessagesTests.cs new file mode 100644 index 0000000..738b852 --- /dev/null +++ b/src/dotnet/PloyBus.Tests/Transport/Transactions/Messages/MessagesTests.cs @@ -0,0 +1,273 @@ +using NUnit.Framework; + +namespace PolyBus.Transport.Transactions.Messages; + +[TestFixture] +public class MessagesTests +{ + private Messages _messages = null!; + + [SetUp] + public void SetUp() + { + _messages = new Messages(); + } + + #region Test Message Classes + + [MessageInfo(MessageType.Command, "OrderService", "CreateOrder", 1, 0, 0)] + public class CreateOrderCommand + { + public string OrderId { get; set; } = string.Empty; + public decimal Amount { get; set; } + } + + [MessageInfo(MessageType.Event, "OrderService", "OrderCreated", 2, 1, 3)] + public class OrderCreatedEvent + { + public string OrderId { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } + } + + [MessageInfo(MessageType.Command, "PaymentService", "ProcessPayment", 1, 5, 2)] + public class ProcessPaymentCommand + { + public string PaymentId { get; set; } = string.Empty; + public decimal Amount { get; set; } + } + + public class MessageWithoutAttribute + { + public string Data { get; set; } = string.Empty; + } + + #endregion + + [Test] + public void Add_ValidMessageType_ReturnsMessageInfo() + { + // Act + var result = _messages.Add(typeof(CreateOrderCommand)); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Type, Is.EqualTo(MessageType.Command)); + Assert.That(result.Endpoint, Is.EqualTo("OrderService")); + Assert.That(result.Name, Is.EqualTo("CreateOrder")); + Assert.That(result.Major, Is.EqualTo(1)); + Assert.That(result.Minor, Is.EqualTo(0)); + Assert.That(result.Patch, Is.EqualTo(0)); + } + + [Test] + public void Add_MessageTypeWithoutAttribute_ThrowsArgumentException() + { + // Act & Assert + var exception = Assert.Throws(() => _messages.Add(typeof(MessageWithoutAttribute))); + Assert.That(exception.Message, Does.Contain("does not have a MessageAttribute")); + Assert.That(exception.Message, Does.Contain(typeof(MessageWithoutAttribute).FullName)); + } + + [Test] + public void GetMessageInfo_ExistingType_ReturnsCorrectMessageInfo() + { + // Arrange + _messages.Add(typeof(CreateOrderCommand)); + + // Act + var result = _messages.GetMessageInfo(typeof(CreateOrderCommand)); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Type, Is.EqualTo(MessageType.Command)); + Assert.That(result.Endpoint, Is.EqualTo("OrderService")); + Assert.That(result.Name, Is.EqualTo("CreateOrder")); + } + + [Test] + public void GetMessageInfo_NonExistentType_ReturnsNull() + { + // Act + var result = _messages.GetMessageInfo(typeof(CreateOrderCommand)); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void GetHeader_ExistingType_ReturnsCorrectHeader() + { + // Arrange + _messages.Add(typeof(OrderCreatedEvent)); + + // Act + var result = _messages.GetHeader(typeof(OrderCreatedEvent)); + + // Assert + Assert.That(result, Is.EqualTo("endpoint=OrderService, type=Event, name=OrderCreated, version=2.1.3")); + } + + [Test] + public void GetHeader_NonExistentType_ReturnsNull() + { + // Act + var result = _messages.GetHeader(typeof(CreateOrderCommand)); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void GetTypeByHeader_ValidHeader_ReturnsCorrectType() + { + // Arrange + _messages.Add(typeof(ProcessPaymentCommand)); + var header = "endpoint=PaymentService, type=Command, name=ProcessPayment, version=1.5.2"; + + // Act + var result = _messages.GetTypeByHeader(header); + + // Assert + Assert.That(result, Is.EqualTo(typeof(ProcessPaymentCommand))); + } + + [Test] + public void GetTypeByHeader_InvalidHeader_ReturnsNull() + { + // Arrange + const string InvalidHeader = "invalid header format"; + + // Act + var result = _messages.GetTypeByHeader(InvalidHeader); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void GetTypeByHeader_NonExistentMessage_ReturnsNull() + { + // Arrange + const string Header = "endpoint=UnknownService, type=Command, name=UnknownCommand, version=1.0.0"; + + // Act + var result = _messages.GetTypeByHeader(Header); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void GetTypeByHeader_CachesResults() + { + // Arrange + _messages.Add(typeof(CreateOrderCommand)); + const string Header = "endpoint=OrderService, type=Command, name=CreateOrder, version=1.0.0"; + + // Act + var result1 = _messages.GetTypeByHeader(Header); + var result2 = _messages.GetTypeByHeader(Header); + + // Assert + Assert.That(result1, Is.EqualTo(typeof(CreateOrderCommand))); + Assert.That(result2, Is.EqualTo(typeof(CreateOrderCommand))); + Assert.That(ReferenceEquals(result1, result2), Is.True); + } + + [Test] + public void GetTypeByMessageInfo_ExistingMessageInfo_ReturnsCorrectType() + { + // Arrange + _messages.Add(typeof(OrderCreatedEvent)); + var messageInfo = new MessageInfo(MessageType.Event, "OrderService", "OrderCreated", 2, 1, 3); + + // Act + var result = _messages.GetTypeByMessageInfo(messageInfo); + + // Assert + Assert.That(result, Is.EqualTo(typeof(OrderCreatedEvent))); + } + + [Test] + public void GetTypeByMessageInfo_NonExistentMessageInfo_ReturnsNull() + { + // Arrange + var messageInfo = new MessageInfo(MessageType.Command, "UnknownService", "UnknownCommand", 1, 0, 0); + + // Act + var result = _messages.GetTypeByMessageInfo(messageInfo); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void GetTypeByMessageInfo_DifferentMinorPatchVersions_ReturnsType() + { + // Arrange + _messages.Add(typeof(OrderCreatedEvent)); // Has version 2.1.3 + var messageInfoDifferentMinor = new MessageInfo(MessageType.Event, "OrderService", "OrderCreated", 2, 5, 3); + var messageInfoDifferentPatch = new MessageInfo(MessageType.Event, "OrderService", "OrderCreated", 2, 1, 9); + + // Act + var result1 = _messages.GetTypeByMessageInfo(messageInfoDifferentMinor); + var result2 = _messages.GetTypeByMessageInfo(messageInfoDifferentPatch); + + // Assert + Assert.That(result1, Is.EqualTo(typeof(OrderCreatedEvent))); + Assert.That(result2, Is.EqualTo(typeof(OrderCreatedEvent))); + } + + [Test] + public void GetTypeByMessageInfo_DifferentMajorVersion_ReturnsNull() + { + // Arrange + _messages.Add(typeof(OrderCreatedEvent)); // Has version 2.1.3 + var messageInfoDifferentMajor = new MessageInfo(MessageType.Event, "OrderService", "OrderCreated", 3, 1, 3); + + // Act + var result = _messages.GetTypeByMessageInfo(messageInfoDifferentMajor); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void MultipleMessages_AllMethodsWorkCorrectly() + { + // Arrange + _messages.Add(typeof(CreateOrderCommand)); + _messages.Add(typeof(OrderCreatedEvent)); + _messages.Add(typeof(ProcessPaymentCommand)); + + // Act & Assert - GetMessageInfo + var commandInfo = _messages.GetMessageInfo(typeof(CreateOrderCommand)); + var eventInfo = _messages.GetMessageInfo(typeof(OrderCreatedEvent)); + var paymentInfo = _messages.GetMessageInfo(typeof(ProcessPaymentCommand)); + + Assert.That(commandInfo?.Type, Is.EqualTo(MessageType.Command)); + Assert.That(eventInfo?.Type, Is.EqualTo(MessageType.Event)); + Assert.That(paymentInfo?.Endpoint, Is.EqualTo("PaymentService")); + + // Act & Assert - GetHeader + var commandHeader = _messages.GetHeader(typeof(CreateOrderCommand)); + var eventHeader = _messages.GetHeader(typeof(OrderCreatedEvent)); + + Assert.That(commandHeader, Does.Contain("OrderService")); + Assert.That(eventHeader, Does.Contain("OrderCreated")); + + // Act & Assert - GetTypeByHeader + var typeFromHeader = _messages.GetTypeByHeader(commandHeader!); + Assert.That(typeFromHeader, Is.EqualTo(typeof(CreateOrderCommand))); + } + + [Test] + public void Add_SameTypeTwice_ThrowsArgumentException() + { + // Arrange + _messages.Add(typeof(CreateOrderCommand)); + + // Act & Assert + Assert.Throws(() => _messages.Add(typeof(CreateOrderCommand))); + } +} diff --git a/src/dotnet/PolyBus.slnx b/src/dotnet/PolyBus.slnx new file mode 100644 index 0000000..d4c9b10 --- /dev/null +++ b/src/dotnet/PolyBus.slnx @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/dotnet/PolyBus/Headers.cs b/src/dotnet/PolyBus/Headers.cs new file mode 100644 index 0000000..3e144c7 --- /dev/null +++ b/src/dotnet/PolyBus/Headers.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; + +namespace PolyBus; + +/// +/// Common header names used in PolyBus. +/// +[DebuggerStepThrough] +public static class Headers +{ + /// + /// The content type header name used for specifying the message content type (e.g., "application/json"). + /// + public const string ContentType = "content-type"; + + /// + /// The message type header name used for specifying the type of the message. + /// + public const string MessageType = "x-type"; +} diff --git a/src/dotnet/PolyBus/IPolyBus.cs b/src/dotnet/PolyBus/IPolyBus.cs new file mode 100644 index 0000000..a59811f --- /dev/null +++ b/src/dotnet/PolyBus/IPolyBus.cs @@ -0,0 +1,29 @@ +using PolyBus.Transport; +using PolyBus.Transport.Transactions; +using PolyBus.Transport.Transactions.Messages; +using PolyBus.Transport.Transactions.Messages.Handlers; + +namespace PolyBus; + +public interface IPolyBus +{ + IDictionary Properties { get; } + + ITransport Transport { get; } + + IList IncomingHandlers { get; } + + IList OutgoingHandlers { get; } + + Messages Messages { get; } + + Task CreateTransaction(IncomingMessage? message = null); + + Task Send(Transaction transaction); + + Task Start(); + + Task Stop(); + + string Name { get; } +} diff --git a/src/dotnet/PolyBus/PolyBus.cs b/src/dotnet/PolyBus/PolyBus.cs new file mode 100644 index 0000000..99a6f6f --- /dev/null +++ b/src/dotnet/PolyBus/PolyBus.cs @@ -0,0 +1,64 @@ +using PolyBus.Transport; +using PolyBus.Transport.Transactions; +using PolyBus.Transport.Transactions.Messages; +using PolyBus.Transport.Transactions.Messages.Handlers; + +namespace PolyBus; + +public class PolyBus(PolyBusBuilder builder) : IPolyBus +{ + public IDictionary Properties => builder.Properties; + + public ITransport Transport { get; set; } = null!; + + public IList IncomingHandlers { get; } = builder.IncomingHandlers; + + public IList OutgoingHandlers { get; } = builder.OutgoingHandlers; + + public Messages Messages { get; } = builder.Messages; + + public Task CreateTransaction(IncomingMessage? message = null) => + builder.TransactionFactory(builder, this, message); + + public async Task Send(Transaction transaction) + { + var step = () => Transport.Send(transaction); + + if (transaction is IncomingTransaction incomingTransaction) + { + var handlers = transaction.Bus.IncomingHandlers; + for (var index = handlers.Count - 1; index >= 0; index--) + { + var handler = handlers[index]; + var next = step; + step = () => handler(incomingTransaction, next); + } + } + else if (transaction is OutgoingTransaction outgoingTransaction) + { + var handlers = transaction.Bus.OutgoingHandlers; + for (var index = handlers.Count - 1; index >= 0; index--) + { + var handler = handlers[index]; + var next = step; + step = () => handler(outgoingTransaction, next); + } + } + + try + { + await step(); + } + catch + { + await transaction.Abort(); + throw; + } + } + + public Task Start() => Transport.Start(); + + public Task Stop() => Transport.Stop(); + + public string Name => builder.Name; +} diff --git a/src/dotnet/PolyBus/PolyBus.csproj b/src/dotnet/PolyBus/PolyBus.csproj new file mode 100644 index 0000000..c5aae2c --- /dev/null +++ b/src/dotnet/PolyBus/PolyBus.csproj @@ -0,0 +1,31 @@ + + + + netstandard2.1 + PolyBus + Poly.Bus + 1.0.0 + Cy Scott + Cy Scott + A polyglot messaging framework for building interoperable applications across multiple programming languages. Native support for TypeScript, Python, and .NET with pluggable transport layer. + messaging;message-bus;polyglot;microservices;distributed-systems;async + MIT + https://github.com/CyAScott/poly-bus + https://github.com/CyAScott/poly-bus + git + README.md + icon.png + false + true + snupkg + true + true + + + + + + + + + diff --git a/src/dotnet/PolyBus/PolyBusBuilder.cs b/src/dotnet/PolyBus/PolyBusBuilder.cs new file mode 100644 index 0000000..9fb10b4 --- /dev/null +++ b/src/dotnet/PolyBus/PolyBusBuilder.cs @@ -0,0 +1,51 @@ +using PolyBus.Transport; +using PolyBus.Transport.InMemory; +using PolyBus.Transport.Transactions; +using PolyBus.Transport.Transactions.Messages; +using PolyBus.Transport.Transactions.Messages.Handlers; + +namespace PolyBus; + +public class PolyBusBuilder +{ + /// + /// The transaction factory will be used to create transactions for message handling. + /// Transactions are used to ensure that a group of message related to a single request + /// are sent to the transport in a single atomic operation. + /// + public TransactionFactory TransactionFactory { get; set; } = (_, bus, message) => + Task.FromResult( + message != null + ? new IncomingTransaction(bus, message) + : new OutgoingTransaction(bus)); + + /// + /// The transport factory will be used to create the transport for the PolyBus instance. + /// The transport is responsible for sending and receiving messages. + /// + public TransportFactory TransportFactory { get; set; } = (builder, bus) => + { + var transport = new InMemoryTransport(); + + return Task.FromResult(transport.AddEndpoint(builder, bus)); + }; + + public Dictionary Properties { get; } = []; + + public IList IncomingHandlers { get; } = []; + + public IList OutgoingHandlers { get; } = []; + + public Messages Messages { get; } = new(); + + public string Name { get; set; } = "PolyBusInstance"; + + public virtual async Task Build() + { + var bus = new PolyBus(this); + + bus.Transport = await TransportFactory(this, bus); + + return bus; + } +} diff --git a/src/dotnet/PolyBus/Transport/ITransport.cs b/src/dotnet/PolyBus/Transport/ITransport.cs new file mode 100644 index 0000000..4db4e8d --- /dev/null +++ b/src/dotnet/PolyBus/Transport/ITransport.cs @@ -0,0 +1,36 @@ +using PolyBus.Transport.Transactions; +using PolyBus.Transport.Transactions.Messages; + +namespace PolyBus.Transport; + +/// +/// An interface for a transport mechanism to send and receive messages. +/// +public interface ITransport +{ + bool SupportsDelayedMessages { get; } + + bool SupportsCommandMessages { get; } + + bool SupportsSubscriptions { get; } + + /// + /// Sends messages associated with the given transaction to the transport. + /// + Task Send(Transaction transaction); + + /// + /// Subscribes to a messages so that the transport can start receiving them. + /// + Task Subscribe(MessageInfo messageInfo); + + /// + /// Enables the transport to start processing messages. + /// + Task Start(); + + /// + /// Stops the transport from processing messages. + /// + Task Stop(); +} diff --git a/src/dotnet/PolyBus/Transport/InMemory/InMemoryTransport.cs b/src/dotnet/PolyBus/Transport/InMemory/InMemoryTransport.cs new file mode 100644 index 0000000..20dcd23 --- /dev/null +++ b/src/dotnet/PolyBus/Transport/InMemory/InMemoryTransport.cs @@ -0,0 +1,159 @@ +using Microsoft.Extensions.Logging; +using PolyBus.Transport.Transactions; +using PolyBus.Transport.Transactions.Messages; + +namespace PolyBus.Transport.InMemory; + +public class InMemoryTransport +{ + public ITransport AddEndpoint(PolyBusBuilder builder, IPolyBus bus) + { + var endpoint = new Endpoint(this, bus); + _endpoints.Add(bus.Name, endpoint); + return endpoint; + } + readonly Dictionary _endpoints = []; + class Endpoint(InMemoryTransport transport, IPolyBus bus) : ITransport + { + public async Task Handle(OutgoingMessage message) + { + if (!transport.UseSubscriptions || _subscriptions.Contains(message.MessageType)) + { + var incomingMessage = new IncomingMessage(bus, message.Body) + { + Headers = message.Headers + }; + + try + { + var transaction = (IncomingTransaction)await bus.CreateTransaction(incomingMessage); + + await transaction.Commit(); + } + catch (Exception error) + { + var logger = incomingMessage.State.Values.OfType().FirstOrDefault(); + logger?.LogError(error, error.Message); + } + } + } + + readonly List _subscriptions = []; + public Task Subscribe(MessageInfo messageInfo) + { + var type = bus.Messages.GetTypeByMessageInfo(messageInfo) + ?? throw new ArgumentException($"Message type for attribute {messageInfo} is not registered."); + _subscriptions.Add(type); + return Task.CompletedTask; + } + + public bool SupportsCommandMessages => true; + + public bool SupportsDelayedMessages => true; + + public bool SupportsSubscriptions => true; + + public Task Send(Transaction transaction) => transport.Send(transaction); + + public Task Start() => transport.Start(); + + public Task Stop() => transport.Stop(); + } + + public async Task Send(Transaction transaction) + { + if (!_active) + { + throw new InvalidOperationException("Transport is not active."); + } + + if (transaction.OutgoingMessages.Count == 0) + { + return; + } + + if (Interlocked.Increment(ref _count) == 1) + { + while (_emptySignal.CurrentCount > 0) + { + await _emptySignal.WaitAsync(0); + } + } + + try + { + var tasks = new List(); + var now = DateTime.UtcNow; + + foreach (var message in transaction.OutgoingMessages) + { + if (message.DeliverAt != null) + { + var wait = message.DeliverAt.Value - now; + if (wait > TimeSpan.Zero) + { + DelayedSendAsync(message, wait); + continue; + } + } + + foreach (var endpoint in _endpoints.Values) + { + var task = endpoint.Handle(message); + tasks.Add(task); + } + } + + await Task.WhenAll(tasks); + } + finally + { + if (Interlocked.Decrement(ref _count) == 0) + { + _emptySignal.Release(); + } + } + } + async void DelayedSendAsync(OutgoingMessage message, TimeSpan delay) + { + try + { + await Task.Delay(delay, _cts.Token); + var transaction = (OutgoingTransaction)await message.Bus.CreateTransaction(); + message.DeliverAt = null; + transaction.OutgoingMessages.Add(message); + await Send(transaction); + } + catch (OperationCanceledException) + { + // Ignore cancellation + } + catch (Exception error) + { + var logger = message.State.Values.OfType().FirstOrDefault(); + logger?.LogError(error, error.Message); + } + } + + public bool UseSubscriptions { get; set; } + + public Task Start() + { + _active = true; + return Task.CompletedTask; + } + bool _active; + + public async Task Stop() + { + _active = false; + _cts.Cancel(); + if (Volatile.Read(ref _count) > 0) + { + await _emptySignal.WaitAsync(); + } + } + int _count; + readonly CancellationTokenSource _cts = new(); + readonly SemaphoreSlim _emptySignal = new(0, 1); +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/IncomingTransaction.cs b/src/dotnet/PolyBus/Transport/Transactions/IncomingTransaction.cs new file mode 100644 index 0000000..8f18980 --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/IncomingTransaction.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; +using PolyBus.Transport.Transactions.Messages; + +namespace PolyBus.Transport.Transactions; + +[DebuggerStepThrough] +public class IncomingTransaction(IPolyBus bus, IncomingMessage incomingMessage) : Transaction(bus) +{ + /// + /// The incoming message from the transport being processed. + /// + public virtual IncomingMessage IncomingMessage { get; set; } = incomingMessage ?? throw new ArgumentNullException(nameof(incomingMessage)); +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/Error/ErrorHandler.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/Error/ErrorHandler.cs new file mode 100644 index 0000000..c82390e --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/Error/ErrorHandler.cs @@ -0,0 +1,65 @@ +namespace PolyBus.Transport.Transactions.Messages.Handlers.Error; + +public class ErrorHandler +{ + public const string ErrorMessageHeader = "X-Error-Message"; + public const string ErrorStackTraceHeader = "X-Error-Stack-Trace"; + public const string RetryCountHeader = "X-Retry-Count"; + + public int Delay { get; set; } = 30; + + public int DelayedRetryCount { get; set; } = 3; + + public int ImmediateRetryCount { get; set; } = 3; + + public string? DeadLetterEndpoint { get; set; } + + public async Task Retrier(IncomingTransaction transaction, Func next) + { + var delayedAttempt = transaction.IncomingMessage.Headers.TryGetValue(RetryCountHeader, out var headerValue) + && byte.TryParse(headerValue, out var parsedHeaderValue) + ? parsedHeaderValue + : 0; + var delayedRetryCount = Math.Max(1, DelayedRetryCount); + var immediateRetryCount = Math.Max(1, ImmediateRetryCount); + + for (var immediateAttempt = 0; immediateAttempt < immediateRetryCount; immediateAttempt++) + { + try + { + await next(); + break; + } + catch (Exception error) + { + transaction.OutgoingMessages.Clear(); + + if (immediateAttempt < immediateRetryCount - 1) + { + continue; + } + + if (delayedAttempt < delayedRetryCount) + { + // Re-queue the message with a delay + delayedAttempt++; + + var delayedMessage = transaction.AddOutgoingMessage( + transaction.IncomingMessage, + transaction.Bus.Name); + delayedMessage.DeliverAt = GetNextRetryTime(delayedAttempt); + delayedMessage.Headers[RetryCountHeader] = delayedAttempt.ToString(); + + continue; + } + + var deadLetterEndpoint = DeadLetterEndpoint ?? $"{transaction.Bus.Name}.Errors"; + var deadLetterMessage = transaction.AddOutgoingMessage(transaction.IncomingMessage, deadLetterEndpoint); + deadLetterMessage.Headers[ErrorMessageHeader] = error.Message; + deadLetterMessage.Headers[ErrorStackTraceHeader] = error.StackTrace ?? string.Empty; + } + } + } + + public virtual DateTime GetNextRetryTime(int attempt) => DateTime.UtcNow.AddSeconds(attempt * Delay); +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/IncomingHandler.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/IncomingHandler.cs new file mode 100644 index 0000000..0a4c0db --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/IncomingHandler.cs @@ -0,0 +1,6 @@ +namespace PolyBus.Transport.Transactions.Messages.Handlers; + +/// +/// A method for handling incoming messages from the transport. +/// +public delegate Task IncomingHandler(IncomingTransaction transaction, Func next); diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/OutgoingHandler.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/OutgoingHandler.cs new file mode 100644 index 0000000..a04c0cb --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/OutgoingHandler.cs @@ -0,0 +1,6 @@ +namespace PolyBus.Transport.Transactions.Messages.Handlers; + +/// +/// A method for handling outgoing messages to the transport. +/// +public delegate Task OutgoingHandler(OutgoingTransaction transaction, Func next); diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/Serializers/JsonHandlers.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/Serializers/JsonHandlers.cs new file mode 100644 index 0000000..5bdf85d --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/Handlers/Serializers/JsonHandlers.cs @@ -0,0 +1,62 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace PolyBus.Transport.Transactions.Messages.Handlers.Serializers; + +public class JsonHandlers +{ + public JsonSerializerOptions? JsonSerializerOptions { get; set; } + + public string ContentType { get; set; } = "application/json"; + + /// + /// If the type header is missing, invalid, or if the type cannot be found, throw an exception. + /// + public bool ThrowOnMissingType { get; set; } = true; + + public Task Deserializer(IncomingTransaction transaction, Func next) + { + var message = transaction.IncomingMessage; + + var type = !message.Headers.TryGetValue(Headers.MessageType, out var header) + ? null + : message.Bus.Messages.GetTypeByHeader(header); + + if (type == null && ThrowOnMissingType) + { + throw new InvalidOperationException("The type header is missing, invalid, or if the type cannot be found."); + } + + message.Message = type == null + ? JsonNode.Parse(message.Body)! + : JsonSerializer.Deserialize(message.Body, type, JsonSerializerOptions)!; + + return next(); + } + + /// + /// If the message type is not in the list of known messages, throw an exception. + /// + public bool ThrowOnInvalidType { get; set; } = true; + + public Task Serializer(OutgoingTransaction transaction, Func next) + { + foreach (var message in transaction.OutgoingMessages) + { + message.Body = JsonSerializer.Serialize(message.Message, JsonSerializerOptions); + message.Headers[Headers.ContentType] = ContentType; + + var header = message.Bus.Messages.GetHeader(message.MessageType); + + if (header != null) + { + message.Headers[Headers.MessageType] = header; + } + else if (ThrowOnInvalidType) + { + throw new InvalidOperationException("The header has an valid type."); + } + } + return next(); + } +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/IncomingMessage.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/IncomingMessage.cs new file mode 100644 index 0000000..c5cd641 --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/IncomingMessage.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace PolyBus.Transport.Transactions.Messages; + +[DebuggerStepThrough] +public class IncomingMessage(IPolyBus bus, string body, object? message = null, Type? messageType = null) : Message(bus) +{ + /// + /// The default is string, but can be changed based on deserialization. + /// + public virtual Type MessageType { get; set; } = messageType ?? typeof(string); + + /// + /// The message body contents. + /// + public virtual string Body { get; set; } = body ?? throw new ArgumentNullException(nameof(body)); + + /// + /// The deserialized message object, otherwise the same value as Body. + /// + public virtual object Message { get; set; } = message ?? body; +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/Message.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/Message.cs new file mode 100644 index 0000000..636bb5e --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/Message.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace PolyBus.Transport.Transactions.Messages; + +[DebuggerStepThrough] +public class Message(IPolyBus bus) +{ + /// + /// State dictionary that can be used to store arbitrary data associated with the message. + /// + public virtual IDictionary State { get; } = new Dictionary(); + + /// + /// Message headers from the transport. + /// + public virtual IDictionary Headers { get; set; } = new Dictionary(); + + /// + /// The bus instance associated with the message. + /// + public IPolyBus Bus => bus ?? throw new ArgumentNullException(nameof(bus)); +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/MessageInfo.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/MessageInfo.cs new file mode 100644 index 0000000..b86330c --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/MessageInfo.cs @@ -0,0 +1,69 @@ +using System.Text.RegularExpressions; + +namespace PolyBus.Transport.Transactions.Messages; + +/// +/// This decorates a message class with metadata about the message. +/// This is used to identify the message type and version so that it can be routed and deserialized appropriately. +/// +/// If the message is a command or event. +/// The endpoint that publishes the event message or the endpoint that handles the command. +/// The unique name for the message for the given endpoint. +/// The major version of the message schema. +/// The minor version of the message schema. +/// The patch version of the message schema. +[AttributeUsage(AttributeTargets.Class)] +public class MessageInfo(MessageType type, string endpoint, string name, int major, int minor, int patch) : Attribute +{ + /// + /// Parses a message attribute from a message header string. + /// + /// + /// If the header is valid, returns a MessageAttribute instance; otherwise, returns null. + /// + public static MessageInfo? GetAttributeFromHeader(string header) + { + var match = _headerPattern.Match(header); + + if (!match.Success) + { + return null; + } + + var endpoint = match.Groups["endpoint"].Value; + var name = match.Groups["name"].Value; + var type = Enum.Parse(match.Groups["type"].Value, true); + var major = Convert.ToInt32(match.Groups["major"].Value); + var minor = Convert.ToInt32(match.Groups["minor"].Value); + var patch = Convert.ToInt32(match.Groups["patch"].Value); + + return new MessageInfo(type, endpoint, name, major, minor, patch); + } + static readonly Regex _headerPattern = new(@"^endpoint\s*=\s*(?[^,\s]+),\s*type\s*=\s*(?[^,\s]+),\s*name\s*=\s*(?[^,\s]+),\s*version\s*=\s*(?\d+)\.(?\d+)\.(?\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public MessageType Type => type; + public int Major => major; + public int Minor => minor; + public int Patch => patch; + public string Endpoint => endpoint; + public string Name => name; + + /// + /// Compares two message attributes for equality. + /// The patch and minor versions are not considered for equality. + /// + public bool Equals(MessageInfo? other) => + other != null + && Type == other.Type + && Endpoint == other.Endpoint + && Name == other.Name + && Major == other.Major; + public override bool Equals(object? obj) => obj is MessageInfo other && Equals(other); + public override int GetHashCode() => HashCode.Combine(Type, Endpoint, Name, Major, Minor, Patch); + + /// + /// Serializes the message attribute to a string format suitable for message headers. + /// + public string ToString(bool includeVersion) => $"endpoint={Endpoint}, type={type}, name={Name}" + (includeVersion ? $", version={major}.{minor}.{patch}" : ""); + public override string ToString() => ToString(true); +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/MessageType.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/MessageType.cs new file mode 100644 index 0000000..32b6712 --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/MessageType.cs @@ -0,0 +1,16 @@ +namespace PolyBus.Transport.Transactions.Messages; + +public enum MessageType +{ + /// + /// Command message type. + /// Commands are messages that are sent to and processed by a single endpoint. + /// + Command, + + /// + /// Event message type. + /// Events are messages that can be processed by multiple endpoints and sent from a single endpoint. + /// + Event +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/Messages.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/Messages.cs new file mode 100644 index 0000000..03273bf --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/Messages.cs @@ -0,0 +1,77 @@ +using System.Collections.Concurrent; +using System.Reflection; + +namespace PolyBus.Transport.Transactions.Messages; + +/// +/// A collection of message types and their associated message headers. +/// +public class Messages +{ + readonly ConcurrentDictionary _map = new(); + readonly Dictionary _types = []; + + /// + /// Gets the message attribute associated with the specified type. + /// + public virtual MessageInfo? GetMessageInfo(Type type) => + _types.TryGetValue(type, out var value) ? value.attribute : null; + + /// + /// Attempts to get the message type associated with the specified header. + /// + /// + /// If found, returns the message type; otherwise, returns null. + /// + public virtual Type? GetTypeByHeader(string header) + { + var attribute = MessageInfo.GetAttributeFromHeader(header); + return attribute == null ? null : _map.GetOrAdd(header, _ => _types + .Where(pair => pair.Value.attribute.Equals(attribute)) + .Select(pair => pair.Key) + .FirstOrDefault()); + } + + /// + /// Attempts to get the message header associated with the specified type. + /// + /// + /// If found, returns the message header; otherwise, returns null. + /// + public virtual string? GetHeader(Type type) => + _types.TryGetValue(type, out var value) ? value.header : null; + + /// + /// Adds a message type to the collection. + /// The message type must have a MessageAttribute defined. + /// + /// + /// The MessageAttribute associated with the message type. + /// + /// + ///Type {messageType.FullName} does not have a MessageAttribute + /// + public virtual MessageInfo Add(Type messageType) + { + var attribute = messageType.GetCustomAttribute() + ?? throw new ArgumentException($"Type {messageType.FullName} does not have a MessageAttribute."); + + var header = attribute.ToString(true); + _types.Add(messageType, (attribute, header)); + _map.TryAdd(header, messageType); + + return attribute; + } + + /// + /// Attempts to get the message type associated with the specified attribute. + /// + /// + /// If found, returns the message type; otherwise, returns null. + /// + public virtual Type? GetTypeByMessageInfo(MessageInfo messageInfo) => + _types + .Where(it => it.Value.attribute.Equals(messageInfo)) + .Select(it => it.Key) + .FirstOrDefault(); +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/Messages/OutgoingMessage.cs b/src/dotnet/PolyBus/Transport/Transactions/Messages/OutgoingMessage.cs new file mode 100644 index 0000000..85a4658 --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Messages/OutgoingMessage.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; + +namespace PolyBus.Transport.Transactions.Messages; + +[DebuggerStepThrough] +public class OutgoingMessage(IPolyBus bus, object message, string endpoint) : Message(bus) +{ + /// + /// If the transport supports delayed messages, this is the time at which the message should be delivered. + /// + public virtual DateTime? DeliverAt { get; set; } + + public virtual Type MessageType { get; set; } = message.GetType(); + + /// + /// The serialized message body contents. + /// + public virtual string Body { get; set; } = message.ToString() ?? string.Empty; + + /// + /// If the message is a command then this is the endpoint the message is being sent to. + /// If the message is an event then this is the source endpoint the message is being sent from. + /// + public virtual string Endpoint { get; set; } = endpoint; + + /// + /// The message object. + /// + public virtual object Message { get; set; } = message; +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/OutgoingTransaction.cs b/src/dotnet/PolyBus/Transport/Transactions/OutgoingTransaction.cs new file mode 100644 index 0000000..6ee1c7a --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/OutgoingTransaction.cs @@ -0,0 +1,6 @@ +using System.Diagnostics; + +namespace PolyBus.Transport.Transactions; + +[DebuggerStepThrough] +public class OutgoingTransaction(IPolyBus bus) : Transaction(bus); diff --git a/src/dotnet/PolyBus/Transport/Transactions/Transaction.cs b/src/dotnet/PolyBus/Transport/Transactions/Transaction.cs new file mode 100644 index 0000000..227d403 --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/Transaction.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; +using PolyBus.Transport.Transactions.Messages; + +namespace PolyBus.Transport.Transactions; + +[DebuggerStepThrough] +public class Transaction(IPolyBus bus) +{ + /// + /// The bus instance associated with the transaction. + /// + public IPolyBus Bus => bus ?? throw new ArgumentNullException(nameof(bus)); + + /// + /// State dictionary that can be used to store arbitrary data associated with the transaction. + /// + public virtual IDictionary State { get; } = new Dictionary(); + + /// + /// A list of outgoing messages to be sent when the transaction is committed. + /// + public virtual IList OutgoingMessages { get; } = []; + + public virtual OutgoingMessage AddOutgoingMessage(object message, string? endpoint = null) + { + string GetEndpoint() + { + var messageInfo = bus.Messages.GetMessageInfo(message.GetType()) + ?? throw new ArgumentException($"Message type {message.GetType().FullName} is not registered."); + return messageInfo.Endpoint; + } + var outgoingMessage = new OutgoingMessage(bus, message, endpoint ?? GetEndpoint()); + OutgoingMessages.Add(outgoingMessage); + return outgoingMessage; + } + + /// + /// If an exception occurs during processing, the transaction will be aborted. + /// + public virtual Task Abort() => Task.CompletedTask; + + /// + /// If no exception occurs during processing, the transaction will be committed. + /// + public virtual Task Commit() => Bus.Send(this); +} diff --git a/src/dotnet/PolyBus/Transport/Transactions/TransactionFactory.cs b/src/dotnet/PolyBus/Transport/Transactions/TransactionFactory.cs new file mode 100644 index 0000000..5208772 --- /dev/null +++ b/src/dotnet/PolyBus/Transport/Transactions/TransactionFactory.cs @@ -0,0 +1,10 @@ +using PolyBus.Transport.Transactions.Messages; + +namespace PolyBus.Transport.Transactions; + +/// +/// A method for creating a new transaction for processing a request. +/// This should be used to integrate with external transaction systems to ensure message processing +/// is done within the context of a transaction. +/// +public delegate Task TransactionFactory(PolyBusBuilder builder, IPolyBus bus, IncomingMessage? message = null); diff --git a/src/dotnet/PolyBus/Transport/TransportFactory.cs b/src/dotnet/PolyBus/Transport/TransportFactory.cs new file mode 100644 index 0000000..8befbbc --- /dev/null +++ b/src/dotnet/PolyBus/Transport/TransportFactory.cs @@ -0,0 +1,6 @@ +namespace PolyBus.Transport; + +/// +/// Creates a transport instance to be used by PolyBus. +/// +public delegate Task TransportFactory(PolyBusBuilder builder, IPolyBus bus); diff --git a/src/dotnet/README.md b/src/dotnet/README.md new file mode 100644 index 0000000..2b83e39 --- /dev/null +++ b/src/dotnet/README.md @@ -0,0 +1,195 @@ +# PolyBus .NET + +A .NET implementation of the PolyBus messaging library, providing a unified interface for message transport across different messaging systems. + +## Prerequisites + +- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) or later +- Any IDE that supports .NET development (Visual Studio, VS Code, JetBrains Rider) + +## Project Structure + +``` +src/dotnet/ +├── PolyBus/ # Main library project +│ ├── PolyBus.csproj # Project file +│ ├── IPolyBus.cs # Main interface +│ ├── PolyBus.cs # Core implementation +│ ├── PolyBusBuilder.cs # Builder pattern implementation +│ ├── Headers.cs # Message headers +│ └── Transport/ # Transport implementations +│ ├── ITransport.cs # Transport interface +│ └── TransportFactory.cs +├── PloyBus.Tests/ # Test project +│ ├── PloyBus.Tests.csproj # Test project file +│ └── PolyBusTests.cs # Test implementations +├── Directory.Build.props # Common build properties +├── PolyBus.slnx # Solution file +└── lint.sh # Code quality script +``` + +## Quick Start + +### Building the Project + +```bash +# Navigate to the dotnet directory +cd src/dotnet + +# Restore dependencies +dotnet restore + +# Build the solution +dotnet build +``` + +### Running Tests + +```bash +# Run all tests +dotnet test + +# Run tests with detailed output +dotnet test --verbosity normal + +# Run tests with code coverage (if coverage tools are installed) +dotnet test --collect:"XPlat Code Coverage" +``` + +### Running Specific Test Projects + +```bash +# Run only the main test project +dotnet test PloyBus.Tests/PloyBus.Tests.csproj + +# Run tests matching a specific pattern +dotnet test --filter "TestMethodName" +``` + +## Development Workflow + +### Code Quality and Linting + +This project includes comprehensive code analysis and formatting tools: + +```bash +# Run the complete linting suite +./lint.sh + +# Check code formatting only +dotnet format --verify-no-changes + +# Auto-fix formatting issues +dotnet format + +# Build with analysis enabled +dotnet build --verbosity normal +``` + +### IDE Integration + +#### Visual Studio Code +1. Install the C# extension +2. Open the `src/dotnet` folder in VS Code +3. Analysis warnings will appear in the Problems panel +4. Use Ctrl+. for quick fixes + +#### Visual Studio / JetBrains Rider +1. Open `PolyBus.slnx` solution file +2. Analysis results appear in Error List / Inspection Results +3. Follow suggested fixes and refactorings + +## Configuration + +### Build Configuration + +The project uses `Directory.Build.props` for common settings: + +- **Target Framework**: .NET 10.0 +- **Code Analysis**: Enabled with recommended rules +- **Nullable Reference Types**: Enabled +- **Documentation Generation**: Enabled +- **Implicit Usings**: Enabled + +### Code Style + +Code style is enforced through: +- Microsoft.CodeAnalysis.NetAnalyzers +- EditorConfig settings (see [LINTING.md](LINTING.md)) +- Built-in .NET formatting rules + +## Dependencies + +### Main Project (PolyBus) +- `Microsoft.Extensions.Logging.Abstractions` (9.0.10) + +### Test Project (PloyBus.Tests) +- `Microsoft.NET.Test.Sdk` (18.0.0) +- `NUnit` (4.4.0) +- `NUnit.Analyzers` (4.11.2) +- `NUnit3TestAdapter` (5.2.0) + +## Common Commands + +```bash +# Clean build artifacts +dotnet clean + +# Restore packages +dotnet restore + +# Build without restore +dotnet build --no-restore + +# Run tests with logger +dotnet test --logger "console;verbosity=detailed" + +# Pack for NuGet (when ready for distribution) +dotnet pack --configuration Release + +# Watch for changes and rebuild +dotnet watch build + +# Watch for changes and rerun tests +dotnet watch test +``` + +## Troubleshooting + +### Build Issues + +1. **Missing SDK**: Ensure .NET 10.0 SDK is installed + ```bash + dotnet --version + ``` + +2. **Package Restore Issues**: Clear NuGet cache + ```bash + dotnet nuget locals all --clear + dotnet restore + ``` + +3. **Analysis Warnings**: See [LINTING.md](LINTING.md) for details on addressing code analysis warnings + +### Test Issues + +1. **Tests Not Discovering**: Ensure test project references are correct +2. **NUnit Issues**: Verify NUnit packages are properly restored + +## Contributing + +1. Follow the established code style (enforced by analyzers) +2. Run `./lint.sh` before committing +3. Ensure all tests pass: `dotnet test` +4. Add tests for new functionality +5. Update documentation as needed + +## Additional Resources + +- [LINTING.md](LINTING.md) - Detailed information about code quality setup +- [.NET Documentation](https://docs.microsoft.com/en-us/dotnet/) +- [NUnit Documentation](https://docs.nunit.org/) + +## License + +See the main project LICENSE file for licensing information. \ No newline at end of file diff --git a/src/dotnet/lint.sh b/src/dotnet/lint.sh new file mode 100755 index 0000000..6feb577 --- /dev/null +++ b/src/dotnet/lint.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# .NET Code Linting Script +# This script runs various code analysis and linting checks for the .NET projects + +echo "🔍 Running .NET Code Analysis..." +echo "======================================" + +# Change to the dotnet source directory +cd "$(dirname "$0")" + +# Build the solution with analysis enabled +echo "" +echo "📦 Building solution with code analysis..." +dotnet build --verbosity quiet --no-restore + +if [ $? -eq 0 ]; then + echo "✅ Build completed successfully with analysis warnings" +else + echo "❌ Build failed - please check errors above" + exit 1 +fi + +# Run code format check +echo "" +echo "🎨 Checking code formatting..." +dotnet format --verify-no-changes --verbosity diagnostic + +if [ $? -eq 0 ]; then + echo "✅ Code formatting is correct" +else + echo "⚠️ Code formatting issues found - run 'dotnet format' to fix" +fi + +# Run security analysis (if available) +echo "" +echo "🔒 Running security analysis..." +if command -v dotnet-security-scan &> /dev/null; then + dotnet security-scan +else + echo "ℹ️ Security scanner not installed - install with: dotnet tool install --global security-scan" +fi + +echo "" +echo "🏁 Code analysis complete!" +echo "To fix formatting issues, run: dotnet format" +echo "To view analysis results in VS Code, check the Problems panel" \ No newline at end of file diff --git a/src/python/.gitignore b/src/python/.gitignore new file mode 100644 index 0000000..826723a --- /dev/null +++ b/src/python/.gitignore @@ -0,0 +1,82 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# macOS +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ \ No newline at end of file diff --git a/src/python/LICENSE b/src/python/LICENSE new file mode 100644 index 0000000..fc9dd6d --- /dev/null +++ b/src/python/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Poly Bus Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/python/README.md b/src/python/README.md new file mode 100644 index 0000000..bb03351 --- /dev/null +++ b/src/python/README.md @@ -0,0 +1,251 @@ +# PolyBus Python + +A Python implementation of the PolyBus messaging library, providing a unified interface for message transport across different messaging systems. + +## Prerequisites + +- [Python 3.8+](https://www.python.org/downloads/) (supports Python 3.8-3.12) +- pip (Python package installer) +- Any IDE that supports Python development (VS Code, PyCharm, etc.) + +## Project Structure + +``` +src/python/ +├── src/ # Source code +│ ├── __init__.py # Package initialization +│ ├── i_poly_bus.py # Main interface +│ ├── poly_bus.py # Core implementation +│ ├── poly_bus_builder.py # Builder pattern implementation +│ ├── headers.py # Message headers +│ └── transport/ # Transport implementations +│ ├── __init__.py +│ └── i_transport.py # Transport interface +├── tests/ # Test package +│ ├── __init__.py +│ ├── test_poly_bus.py # Test implementations +│ └── transport/ # Transport tests +├── pyproject.toml # Project configuration and dependencies +├── requirements-dev.txt # Development dependencies +├── conftest.py # Pytest configuration +├── dev.sh # Development workflow script +└── setup.py # Legacy setup script +``` + +## Quick Start + +### Setting Up Development Environment + +```bash +# Navigate to the python directory +cd src/python + +# Create a virtual environment (recommended) +python3 -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install the package in development mode with dev dependencies +./dev.sh install +# Or manually: +pip install -e ".[dev]" +``` + +### Building the Project + +```bash +# Install dependencies and package +./dev.sh install + +# Build the package +./dev.sh build +``` + +### Running Tests + +```bash +# Run all tests +./dev.sh test +# Or: python -m pytest + +# Run tests with coverage +./dev.sh test-cov +# Or: python -m pytest --cov=poly_bus --cov-report=html + +# Run specific test files +python -m pytest tests/test_poly_bus.py + +# Run tests with verbose output +python -m pytest -v + +# Run tests matching a pattern +python -m pytest -k "test_pattern" +``` + +## Development Workflow + +### Code Quality and Linting + +This project includes comprehensive code analysis and formatting tools: + +```bash +# Run the complete development check suite +./dev.sh check + +# Format code automatically +./dev.sh format + +# Run linters only +./dev.sh lint + +# Individual tools: +python -m black src tests # Code formatting +python -m isort src tests # Import sorting +python -m flake8 src tests # Style checking +python -m mypy src # Type checking +``` + +### IDE Integration + +#### Visual Studio Code +1. Install the Python extension +2. Install Python development extensions (Black, isort, Flake8, mypy) +3. Open the `src/python` folder in VS Code +4. The project includes configuration for auto-formatting and linting + +#### PyCharm +1. Open the `src/python` folder as a project +2. Configure the virtual environment as the project interpreter +3. Enable code inspections and formatting tools + +## Configuration + +### Project Configuration + +The project uses `pyproject.toml` for modern Python packaging: + +- **Python Version**: 3.8+ (supports 3.8-3.12) +- **Build System**: setuptools +- **Testing**: pytest with coverage +- **Code Quality**: black, isort, flake8, mypy +- **Package Structure**: src layout + +### Code Style + +Code style is enforced through: +- **Black** (88 character line length) +- **isort** (import sorting with black profile) +- **flake8** (PEP 8 compliance) +- **mypy** (type checking with strict settings) + +### Testing Configuration + +Pytest configuration includes: +- Coverage reporting (HTML, XML, terminal) +- Strict marker and config validation +- Support for async tests (pytest-asyncio) +- Test discovery patterns + +## Dependencies + +### Runtime Dependencies +- No runtime dependencies (pure Python implementation) + +### Development Dependencies +- `pytest>=7.0.0` - Testing framework +- `pytest-cov>=4.0.0` - Coverage reporting +- `pytest-asyncio>=0.21.0` - Async test support +- `black>=23.0.0` - Code formatting +- `isort>=5.12.0` - Import sorting +- `flake8>=6.0.0` - Style checking +- `mypy>=1.0.0` - Type checking + +## Common Commands + +```bash +# Development script commands +./dev.sh install # Install in development mode +./dev.sh test # Run tests +./dev.sh test-cov # Run tests with coverage +./dev.sh lint # Run all linters +./dev.sh format # Format code +./dev.sh check # Run all checks (format + lint + test) +./dev.sh clean # Clean build artifacts +./dev.sh build # Build package +./dev.sh help # Show all available commands + +# Direct pytest commands +python -m pytest # Run all tests +python -m pytest --cov-report=html # Generate HTML coverage report +python -m pytest tests/test_poly_bus.py # Run specific test file +python -m pytest -x # Stop on first failure +python -m pytest --lf # Run last failed tests only + +# Package management +pip install -e ".[dev]" # Install in development mode +pip install -r requirements-dev.txt # Install dev dependencies only +python -m build # Build wheel and source distribution +``` + +## Troubleshooting + +### Environment Issues + +1. **Python Version**: Ensure Python 3.8+ is installed + ```bash + python3 --version + ``` + +2. **Virtual Environment**: Always use a virtual environment + ```bash + python3 -m venv venv + source venv/bin/activate + ``` + +3. **Package Installation Issues**: Upgrade pip and setuptools + ```bash + pip install --upgrade pip setuptools wheel + ``` + +### Test Issues + +1. **Import Errors**: Ensure package is installed in development mode + ```bash + pip install -e ".[dev]" + ``` + +2. **Coverage Issues**: Check that source paths are correct in `pyproject.toml` + +3. **Type Checking Issues**: mypy configuration is strict; add type annotations as needed + +### Code Quality Issues + +1. **Formatting**: Run `./dev.sh format` to auto-fix most formatting issues +2. **Import Order**: isort will automatically fix import ordering +3. **Type Errors**: Add proper type annotations for mypy compliance + +## Contributing + +1. Follow the established code style (enforced by formatters and linters) +2. Run `./dev.sh check` before committing +3. Ensure all tests pass and maintain high coverage +4. Add tests for new functionality +5. Add type annotations for all new code +6. Update documentation as needed + +## Coverage Reports + +After running tests with coverage (`./dev.sh test-cov`): +- **Terminal**: Coverage summary displayed in terminal +- **HTML**: Detailed report available in `htmlcov/index.html` +- **XML**: Machine-readable report in `coverage.xml` + +## Additional Resources + +- [Python Packaging Guide](https://packaging.python.org/) +- [pytest Documentation](https://docs.pytest.org/) +- [Black Code Formatter](https://black.readthedocs.io/) +- [mypy Type Checking](https://mypy.readthedocs.io/) + +## License + +See the main project LICENSE file for licensing information. \ No newline at end of file diff --git a/src/python/conftest.py b/src/python/conftest.py new file mode 100644 index 0000000..ed5ed5c --- /dev/null +++ b/src/python/conftest.py @@ -0,0 +1,9 @@ +# Pytest configuration file +"""Configuration for pytest.""" + +import sys +from pathlib import Path + +# Add src directory to Python path for imports +src_path = Path(__file__).parent / "src" +sys.path.insert(0, str(src_path)) \ No newline at end of file diff --git a/src/python/dev.sh b/src/python/dev.sh new file mode 100755 index 0000000..9d56be2 --- /dev/null +++ b/src/python/dev.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Development script for poly-bus Python project +# Usage: ./dev.sh [command] + +set -e + +PYTHON=${PYTHON:-python3} +PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +cd "$PROJECT_DIR" + +case "${1:-help}" in + "install") + echo "Installing poly-bus in development mode..." + $PYTHON -m pip install --upgrade pip setuptools wheel + $PYTHON -m pip install -e ".[dev]" + ;; + "test") + echo "Running tests..." + $PYTHON -m pytest + ;; + "test-cov") + echo "Running tests with coverage..." + $PYTHON -m pytest --cov=src --cov-report=html --cov-report=term + ;; + "lint") + echo "Running linters..." + echo " - flake8..." + $PYTHON -m flake8 src tests + echo " - mypy..." + $PYTHON -m mypy src + echo "Linting complete!" + ;; + "format") + echo "Formatting code..." + $PYTHON -m black src tests + $PYTHON -m isort src tests + echo "Formatting complete!" + ;; + "check") + echo "Running all checks..." + ./dev.sh format + ./dev.sh lint + ./dev.sh test-cov + echo "All checks passed!" + ;; + "clean") + echo "Cleaning build artifacts..." + rm -rf build/ dist/ *.egg-info/ .coverage htmlcov/ .pytest_cache/ .mypy_cache/ + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + echo "Clean complete!" + ;; + "build") + echo "Building package..." + $PYTHON -m pip install --upgrade build + $PYTHON -m build + echo "Build complete!" + ;; + "help"|*) + echo "poly-bus development script" + echo "" + echo "Usage: ./dev.sh [command]" + echo "" + echo "Commands:" + echo " install Install package in development mode" + echo " test Run tests" + echo " test-cov Run tests with coverage" + echo " lint Run linters (flake8, mypy)" + echo " format Format code (black, isort)" + echo " check Run all checks (format, lint, test)" + echo " clean Clean build artifacts" + echo " build Build package" + echo " help Show this help" + ;; +esac \ No newline at end of file diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml new file mode 100644 index 0000000..7972588 --- /dev/null +++ b/src/python/pyproject.toml @@ -0,0 +1,119 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "poly-bus" +version = "1.0.0" +description = "A polyglot message bus library" +readme = "README.md" +license = {text = "MIT"} +authors = [ + { name = "Cy Scott", email = "cy.a.scott@live.com" } +] +keywords = ["messaging", "message-bus", "polyglot", "microservices", "distributed-systems", "async"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", +] +requires-python = ">=3.8" +dependencies = [] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", + "pytest-asyncio>=0.21.0", + "black>=23.0.0", + "isort>=5.12.0", + "flake8>=6.0.0", + "mypy>=1.0.0", +] + +[project.urls] +Homepage = "https://github.com/CyAScott/poly-bus" +Repository = "https://github.com/CyAScott/poly-bus" +Issues = "https://github.com/CyAScott/poly-bus/issues" + +[tool.setuptools.packages.find] +where = ["src"] +include = ["*"] +exclude = ["tests*"] + +[tool.setuptools.package-dir] +"" = "src" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "--strict-markers", + "--strict-config", + "--cov=src", + "--cov-report=term-missing", + "--cov-report=html", + "--cov-report=xml", +] +markers = [ + "asyncio: marks tests as async (pytest-asyncio)", +] + +[tool.black] +line-length = 88 +target-version = ["py38"] + +[tool.isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 88 + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true + +[tool.coverage.run] +source = ["src"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/__init__.py", # Exclude __init__.py files (often empty) + "*/i_*.py", # Exclude interfaces + "*/*_handler.py", # Exclude callables + "*/*_factory.py", # Exclude callables +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] \ No newline at end of file diff --git a/src/python/requirements-dev.txt b/src/python/requirements-dev.txt new file mode 100644 index 0000000..ef83574 --- /dev/null +++ b/src/python/requirements-dev.txt @@ -0,0 +1,9 @@ +# Development dependencies requirements +# Install with: pip install -r requirements-dev.txt + +pytest>=7.0.0 +pytest-cov>=4.0.0 +black>=23.0.0 +isort>=5.12.0 +flake8>=6.0.0 +mypy>=1.0.0 \ No newline at end of file diff --git a/src/python/run_error_handler_tests.sh b/src/python/run_error_handler_tests.sh new file mode 100755 index 0000000..33bfcee --- /dev/null +++ b/src/python/run_error_handler_tests.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Test runner script for PolyBus Python error handlers + +echo "Running Python error handler tests..." + +# Run tests with coverage and verbose output +/Users/cyscott/Library/Python/3.9/bin/pytest tests/transport/transaction/message/handlers/error/test_error_handlers.py -v --no-cov + +echo "Test run completed." \ No newline at end of file diff --git a/src/python/setup.py.backup b/src/python/setup.py.backup new file mode 100644 index 0000000..4faf5b5 --- /dev/null +++ b/src/python/setup.py.backup @@ -0,0 +1,9 @@ +from setuptools import setup, find_packages + +setup( + name="poly-bus", + use_scm_version=True, + setup_requires=["setuptools_scm"], + packages=find_packages(where="src"), + package_dir={"": "src"}, +) \ No newline at end of file diff --git a/src/python/src/__init__.py b/src/python/src/__init__.py new file mode 100644 index 0000000..a3cd356 --- /dev/null +++ b/src/python/src/__init__.py @@ -0,0 +1,11 @@ +"""Poly Bus - A polyglot message bus library.""" + +__version__ = "0.1.0" +__author__ = "Poly Bus Contributors" +__email__ = "cy.a.scott@live.com" + +# Main entry points +from .poly_bus_builder import PolyBusBuilder +from .i_poly_bus import IPolyBus + +__all__ = ["PolyBusBuilder", "IPolyBus"] \ No newline at end of file diff --git a/src/python/src/headers.py b/src/python/src/headers.py new file mode 100644 index 0000000..41a9c16 --- /dev/null +++ b/src/python/src/headers.py @@ -0,0 +1,15 @@ +""" +Common header names used in PolyBus. +""" + + +class Headers: + """ + Common header names used in PolyBus. + """ + + #: The content type header name used for specifying the message content type (e.g., "application/json"). + CONTENT_TYPE = "content-type" + + #: The message type header name used for specifying the type of the message. + MESSAGE_TYPE = "x-type" \ No newline at end of file diff --git a/src/python/src/i_poly_bus.py b/src/python/src/i_poly_bus.py new file mode 100644 index 0000000..3d44da4 --- /dev/null +++ b/src/python/src/i_poly_bus.py @@ -0,0 +1,83 @@ +"""PolyBus interface for the Python implementation.""" + +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from src.transport.i_transport import ITransport + from src.transport.transaction.message.handlers.incoming_handler import IncomingHandler + from src.transport.transaction.message.handlers.outgoing_handler import OutgoingHandler + from src.transport.transaction.message.incoming_message import IncomingMessage + from src.transport.transaction.message.messages import Messages + from src.transport.transaction.transaction import Transaction + + +class IPolyBus(ABC): + """Interface for a PolyBus instance that provides message handling and transport functionality.""" + + @property + @abstractmethod + def properties(self) -> Dict[str, Any]: + """The properties associated with this bus instance.""" + pass + + @property + @abstractmethod + def transport(self) -> 'ITransport': + """The transport mechanism used by this bus instance.""" + pass + + @property + @abstractmethod + def incoming_handlers(self) -> 'List[IncomingHandler]': + """Collection of handlers for processing incoming messages.""" + pass + + @property + @abstractmethod + def outgoing_handlers(self) -> 'List[OutgoingHandler]': + """Collection of handlers for processing outgoing messages.""" + pass + + @property + @abstractmethod + def messages(self) -> 'Messages': + """Collection of message types and their associated headers.""" + pass + + @property + @abstractmethod + def name(self) -> str: + """The name of this bus instance.""" + pass + + @abstractmethod + async def create_transaction(self, message: 'Optional[IncomingMessage]' = None) -> 'Transaction': + """Creates a new transaction, optionally based on an incoming message. + + Args: + message: Optional incoming message to create the transaction from. + + Returns: + A new transaction instance. + """ + pass + + @abstractmethod + async def send(self, transaction: 'Transaction') -> None: + """Sends messages associated with the given transaction to the transport. + + Args: + transaction: The transaction containing messages to send. + """ + pass + + @abstractmethod + async def start(self) -> None: + """Enables the bus to start processing messages.""" + pass + + @abstractmethod + async def stop(self) -> None: + """Stops the bus from processing messages.""" + pass \ No newline at end of file diff --git a/src/python/src/poly_bus.py b/src/python/src/poly_bus.py new file mode 100644 index 0000000..b9a569c --- /dev/null +++ b/src/python/src/poly_bus.py @@ -0,0 +1,125 @@ +"""PolyBus implementation for the Python version.""" + +from typing import Dict, Any +from src.i_poly_bus import IPolyBus +from src.transport.transaction.incoming_transaction import IncomingTransaction +from src.transport.transaction.outgoing_transaction import OutgoingTransaction + + +class PolyBus(IPolyBus): + """Main PolyBus implementation that provides message handling and transport functionality.""" + + def __init__(self, builder): + """ + Initialize a PolyBus instance from a builder. + + Args: + builder: The PolyBusBuilder containing the configuration + """ + self._builder = builder + self._transport = None + + @property + def properties(self) -> Dict[str, Any]: + """The properties associated with this bus instance.""" + return self._builder.properties + + @property + def transport(self): + """The transport mechanism used by this bus instance.""" + if self._transport is None: + raise RuntimeError("Transport has not been initialized") + return self._transport + + @transport.setter + def transport(self, value): + """Set the transport mechanism used by this bus instance.""" + self._transport = value + + @property + def incoming_handlers(self): + """Collection of handlers for processing incoming messages.""" + return self._builder.incoming_handlers + + @property + def outgoing_handlers(self): + """Collection of handlers for processing outgoing messages.""" + return self._builder.outgoing_handlers + + @property + def messages(self): + """Collection of message types and their associated headers.""" + return self._builder.messages + + @property + def name(self) -> str: + """The name of this bus instance.""" + return self._builder.name + + async def create_transaction(self, message=None): + """Creates a new transaction, optionally based on an incoming message. + + Args: + message: Optional incoming message to create the transaction from. + + Returns: + A new transaction instance. + """ + return await self._builder.transaction_factory(self._builder, self, message) + + async def send(self, transaction) -> None: + """Sends messages associated with the given transaction to the transport. + + Args: + transaction: The transaction containing messages to send. + """ + async def final_step(): + """Final step that actually sends to transport.""" + await self.transport.send(transaction) + + step = final_step + + # Build handler chain based on transaction type + if isinstance(transaction, IncomingTransaction): + # Process incoming handlers in reverse order + handlers = self.incoming_handlers + for i in range(len(handlers) - 1, -1, -1): + handler = handlers[i] + next_step = step + + # Fix closure issue by using default parameters + def create_step(h=handler, next_fn=next_step): + async def handler_step(): + await h(transaction, next_fn) + return handler_step + + step = create_step() + + elif isinstance(transaction, OutgoingTransaction): + # Process outgoing handlers in reverse order + handlers = self.outgoing_handlers + for i in range(len(handlers) - 1, -1, -1): + handler = handlers[i] + next_step = step + + # Fix closure issue by using default parameters + def create_step(h=handler, next_fn=next_step): + async def handler_step(): + await h(transaction, next_fn) + return handler_step + + step = create_step() + + try: + await step() + except Exception: + await transaction.abort() + raise + + async def start(self) -> None: + """Enables the bus to start processing messages.""" + await self.transport.start() + + async def stop(self) -> None: + """Stops the bus from processing messages.""" + await self.transport.stop() diff --git a/src/python/src/poly_bus_builder.py b/src/python/src/poly_bus_builder.py new file mode 100644 index 0000000..aa2ab42 --- /dev/null +++ b/src/python/src/poly_bus_builder.py @@ -0,0 +1,88 @@ +"""PolyBus builder implementation for the Python version.""" + +from typing import Dict, Any, Optional, TYPE_CHECKING +from src.transport.transaction.incoming_transaction import IncomingTransaction +from src.transport.transaction.message.messages import Messages +from src.transport.transaction.outgoing_transaction import OutgoingTransaction + +if TYPE_CHECKING: + from src.poly_bus import PolyBus + + +async def _default_transaction_factory(builder: 'PolyBusBuilder', bus, message: Optional = None): + """ + Default transaction factory implementation. + + Args: + builder: The PolyBus builder instance (not used in default implementation) + bus: The PolyBus instance + message: The incoming message to process, if any + + Returns: + A new transaction instance + """ + + if message is not None: + return IncomingTransaction(bus, message) + else: + return OutgoingTransaction(bus) + + +async def _default_transport_factory(builder: 'PolyBusBuilder', bus: 'PolyBus'): + """ + Default transport factory implementation - creates an InMemoryTransport. + + Args: + builder: The PolyBus builder instance + bus: The PolyBus instance + + Returns: + An InMemoryTransport endpoint for the bus + """ + from .transport.in_memory.in_memory_transport import InMemoryTransport + + transport = InMemoryTransport() + return transport.add_endpoint(builder, bus) + + +class PolyBusBuilder: + """Builder class for creating PolyBus instances with configurable components.""" + + def __init__(self): + """Initialize a new PolyBusBuilder with default settings.""" + # The transaction factory will be used to create transactions for message handling. + # Transactions are used to ensure that a group of message related to a single request + # are sent to the transport in a single atomic operation. + self.transaction_factory = _default_transaction_factory + + # The transport factory will be used to create the transport for the PolyBus instance. + # The transport is responsible for sending and receiving messages. + self.transport_factory = _default_transport_factory + + # Properties that can be used to store arbitrary data associated with the bus instance + self.properties: Dict[str, Any] = {} + + # Collection of handlers for processing incoming messages + self.incoming_handlers = [] + + # Collection of handlers for processing outgoing messages + self.outgoing_handlers = [] + + # Collection of message types and their associated headers + self.messages = Messages() + + # The name of the bus instance + self.name: str = "PolyBusInstance" + + async def build(self) -> 'PolyBus': + """ + Build and configure a new PolyBus instance. + + Returns: + A configured PolyBus instance ready for use + """ + from src.poly_bus import PolyBus + + bus = PolyBus(self) + bus.transport = await self.transport_factory(self, bus) + return bus diff --git a/src/python/src/transport/__init__.py b/src/python/src/transport/__init__.py new file mode 100644 index 0000000..1e6a782 --- /dev/null +++ b/src/python/src/transport/__init__.py @@ -0,0 +1 @@ +"""Transport layer for PolyBus.""" \ No newline at end of file diff --git a/src/python/src/transport/i_transport.py b/src/python/src/transport/i_transport.py new file mode 100644 index 0000000..b9564cc --- /dev/null +++ b/src/python/src/transport/i_transport.py @@ -0,0 +1,55 @@ +"""Transport interface for the PolyBus Python implementation.""" + +from abc import ABC, abstractmethod +from src.transport.transaction.transaction import Transaction +from src.transport.transaction.message.message_info import MessageInfo + + +class ITransport(ABC): + """An interface for a transport mechanism to send and receive messages.""" + + @property + @abstractmethod + def supports_delayed_messages(self) -> bool: + """Whether this transport supports delayed message delivery.""" + pass + + @property + @abstractmethod + def supports_command_messages(self) -> bool: + """Whether this transport supports command messages.""" + pass + + @property + @abstractmethod + def supports_subscriptions(self) -> bool: + """Whether this transport supports message subscriptions.""" + pass + + @abstractmethod + async def send(self, transaction: 'Transaction') -> None: + """Sends messages associated with the given transaction to the transport. + + Args: + transaction: The transaction containing messages to send. + """ + pass + + @abstractmethod + async def subscribe(self, message_info: 'MessageInfo') -> None: + """Subscribes to messages so that the transport can start receiving them. + + Args: + message_info: Information about the message type to subscribe to. + """ + pass + + @abstractmethod + async def start(self) -> None: + """Enables the transport to start processing messages.""" + pass + + @abstractmethod + async def stop(self) -> None: + """Stops the transport from processing messages.""" + pass \ No newline at end of file diff --git a/src/python/src/transport/in_memory/__init__.py b/src/python/src/transport/in_memory/__init__.py new file mode 100644 index 0000000..b7e9d88 --- /dev/null +++ b/src/python/src/transport/in_memory/__init__.py @@ -0,0 +1 @@ +"""In-memory transport package.""" \ No newline at end of file diff --git a/src/python/src/transport/in_memory/in_memory_transport.py b/src/python/src/transport/in_memory/in_memory_transport.py new file mode 100644 index 0000000..1c10bc4 --- /dev/null +++ b/src/python/src/transport/in_memory/in_memory_transport.py @@ -0,0 +1,212 @@ +"""In-memory transport implementation for PolyBus Python.""" + +import asyncio +import datetime +import logging +from datetime import datetime, timedelta, timezone +import threading +from typing import Dict, List, Type +from uuid import uuid1 +from src.transport.i_transport import ITransport +from src.transport.transaction.transaction import Transaction +from src.transport.transaction.message.incoming_message import IncomingMessage +from src.transport.transaction.message.outgoing_message import OutgoingMessage +from src.transport.transaction.message.message_info import MessageInfo + + +class InMemoryTransport: + """In-memory transport that can handle multiple bus endpoints.""" + + def __init__(self): + self._endpoints: Dict[str, 'Endpoint'] = {} + self._active = False + self._cancellation_token = None + self._tasks: Dict[str, asyncio.Task] = {} + self._tasks_lock = threading.Lock() + self.use_subscriptions = False + + def add_endpoint(self, builder, bus) -> ITransport: + """Add a new endpoint for the given bus. + + Args: + builder: PolyBusBuilder instance (not used in current implementation) + bus: IPolyBus instance + + Returns: + ITransport endpoint for the bus + """ + endpoint = Endpoint(self, bus) + self._endpoints[bus.name] = endpoint + return endpoint + + async def send(self, transaction: Transaction) -> None: + """Send messages from a transaction to all endpoints. + + Args: + transaction: Transaction containing outgoing messages + + Raises: + RuntimeError: If transport is not active + """ + if not self._active: + raise RuntimeError("Transport is not active.") + + if not transaction.outgoing_messages: + return + + task_id = uuid1() + tasks = [] + now = datetime.now(timezone.utc) + + try: + for message in transaction.outgoing_messages: + if message.deliver_at is not None: + wait_time = message.deliver_at - now + if wait_time.total_seconds() > 0: + # Schedule delayed send + schedule_task_id = uuid1() + task = asyncio.create_task(self._delayed_send_async(schedule_task_id, message, wait_time)) + with self._tasks_lock: + self._tasks[schedule_task_id] = task + continue + + # Send to all endpoints immediately + for endpoint in self._endpoints.values(): + task = endpoint.handle(message) + tasks.append(task) + + if tasks: + task = asyncio.gather(*tasks) + with self._tasks_lock: + self._tasks[task_id] = task + finally: + # Clean up any completed delayed tasks + with self._tasks_lock: + if task_id in self._tasks: + del self._tasks[task_id] + + async def _delayed_send_async(self, task_id: str, message: OutgoingMessage, delay: timedelta) -> None: + """Send a message after the specified delay. + + Args: + message: The message to send + delay: How long to wait before sending + """ + try: + await asyncio.sleep(delay.total_seconds()) + transaction = await message.bus.create_transaction() + message.deliver_at = None + transaction.outgoing_messages.append(message) + await self.send(transaction) + except asyncio.CancelledError: + # Ignore cancellation + pass + except Exception as error: + # Try to find a logger in the message state + logger = None + for value in message.state.values(): + if isinstance(value, logging.Logger): + logger = value + break + + if logger: + logger.error(f"Error in delayed send: {error}", exc_info=True) + finally: + # Remove task from tracking dictionary + with self._transport._tasks_lock: + if task_id in self._transport._tasks: + del self._transport._tasks[task_id] + + async def start(self) -> None: + """Start the transport.""" + self._active = True + # Don't set cancellation token to current task - only for transport-internal tasks + self._cancellation_token = None + + async def stop(self) -> None: + """Stop the transport and wait for all pending operations.""" + self._active = False + + with self._tasks_lock: + tasks_to_cancel = list(self._tasks.values()) + + for task in tasks_to_cancel: + task.cancel() + + +class Endpoint(ITransport): + """Transport endpoint for a specific bus instance.""" + + def __init__(self, transport: InMemoryTransport, bus): + self._transport = transport + self._bus = bus + self._subscriptions: List[Type] = [] + + async def handle(self, message: OutgoingMessage) -> None: + """Handle an incoming message from another endpoint. + + Args: + message: The outgoing message from another endpoint + """ + if not self._transport.use_subscriptions or message.message_type in self._subscriptions: + incoming_message = IncomingMessage(self._bus, message.body) + incoming_message.headers = message.headers + + try: + transaction = await self._bus.create_transaction(incoming_message) + await transaction.commit() + except Exception as error: + # Try to find a logger in the message state + logger = None + for value in incoming_message.state.values(): + if isinstance(value, logging.Logger): + logger = value + break + + if logger: + logger.error(f"Error handling message: {error}", exc_info=True) + + async def subscribe(self, message_info: MessageInfo) -> None: + """Subscribe to messages of a specific type. + + Args: + message_info: Information about the message type to subscribe to + + Raises: + ValueError: If message type is not registered + """ + message_type = self._bus.messages.get_type_by_message_info(message_info) + if message_type is None: + raise ValueError(f"Message type for attribute {message_info} is not registered.") + self._subscriptions.append(message_type) + + @property + def supports_command_messages(self) -> bool: + """Whether this transport supports command messages.""" + return True + + @property + def supports_delayed_messages(self) -> bool: + """Whether this transport supports delayed message delivery.""" + return True + + @property + def supports_subscriptions(self) -> bool: + """Whether this transport supports message subscriptions.""" + return True + + async def send(self, transaction: Transaction) -> None: + """Send messages through the transport. + + Args: + transaction: Transaction containing messages to send + """ + await self._transport.send(transaction) + + async def start(self) -> None: + """Start the transport endpoint.""" + await self._transport.start() + + async def stop(self) -> None: + """Stop the transport endpoint.""" + await self._transport.stop() \ No newline at end of file diff --git a/src/python/src/transport/transaction/__init__.py b/src/python/src/transport/transaction/__init__.py new file mode 100644 index 0000000..da6c32a --- /dev/null +++ b/src/python/src/transport/transaction/__init__.py @@ -0,0 +1 @@ +"""Transaction handling for PolyBus transport.""" \ No newline at end of file diff --git a/src/python/src/transport/transaction/incoming_transaction.py b/src/python/src/transport/transaction/incoming_transaction.py new file mode 100644 index 0000000..2240c13 --- /dev/null +++ b/src/python/src/transport/transaction/incoming_transaction.py @@ -0,0 +1,29 @@ +"""Incoming transaction class for PolyBus Python implementation.""" + +from src.transport.transaction.transaction import Transaction +from src.transport.transaction.message.incoming_message import IncomingMessage +from src.i_poly_bus import IPolyBus + + +class IncomingTransaction(Transaction): + """Represents an incoming transaction in the PolyBus system.""" + + def __init__(self, bus: IPolyBus, incoming_message: IncomingMessage): + """Initialize an incoming transaction. + + Args: + bus: The PolyBus instance associated with this transaction. + incoming_message: The incoming message from the transport being processed. + + Raises: + ValueError: If incoming_message is None. + """ + super().__init__(bus) + if incoming_message is None: + raise ValueError("incoming_message cannot be None") + self._incoming_message = incoming_message + + @property + def incoming_message(self) -> IncomingMessage: + """The incoming message from the transport being processed.""" + return self._incoming_message diff --git a/src/python/src/transport/transaction/message/__init__.py b/src/python/src/transport/transaction/message/__init__.py new file mode 100644 index 0000000..2a84b13 --- /dev/null +++ b/src/python/src/transport/transaction/message/__init__.py @@ -0,0 +1 @@ +"""Message handling for PolyBus transactions.""" \ No newline at end of file diff --git a/src/python/src/transport/transaction/message/handlers/__init__.py b/src/python/src/transport/transaction/message/handlers/__init__.py new file mode 100644 index 0000000..536bec6 --- /dev/null +++ b/src/python/src/transport/transaction/message/handlers/__init__.py @@ -0,0 +1 @@ +"""Message handlers for PolyBus.""" \ No newline at end of file diff --git a/src/python/src/transport/transaction/message/handlers/error/__init__.py b/src/python/src/transport/transaction/message/handlers/error/__init__.py new file mode 100644 index 0000000..c4c9e51 --- /dev/null +++ b/src/python/src/transport/transaction/message/handlers/error/__init__.py @@ -0,0 +1 @@ +"""Error handlers for PolyBus messages.""" \ No newline at end of file diff --git a/src/python/src/transport/transaction/message/handlers/error/error_handlers.py b/src/python/src/transport/transaction/message/handlers/error/error_handlers.py new file mode 100644 index 0000000..4817fc7 --- /dev/null +++ b/src/python/src/transport/transaction/message/handlers/error/error_handlers.py @@ -0,0 +1,115 @@ +"""Error handling with retry logic for PolyBus Python implementation.""" + +from datetime import datetime, timedelta, timezone +from typing import Callable, Awaitable, Optional +from src.transport.transaction.incoming_transaction import IncomingTransaction + + +class ErrorHandler: + """Provides error handling and retry logic for message processing.""" + + ERROR_MESSAGE_HEADER = "X-Error-Message" + ERROR_STACK_TRACE_HEADER = "X-Error-Stack-Trace" + RETRY_COUNT_HEADER = "X-Retry-Count" + + def __init__( + self, + delay: int = 30, + delayed_retry_count: int = 3, + immediate_retry_count: int = 3, + dead_letter_endpoint: Optional[str] = None + ): + """Initialize the error handler. + + Args: + delay: Base delay in seconds between delayed retries + delayed_retry_count: Number of delayed retry attempts + immediate_retry_count: Number of immediate retry attempts + dead_letter_endpoint: Optional endpoint for dead letter messages + """ + self.delay = delay + self.delayed_retry_count = delayed_retry_count + self.immediate_retry_count = immediate_retry_count + self.dead_letter_endpoint = dead_letter_endpoint + + async def retrier( + self, + transaction: IncomingTransaction, + next_handler: Callable[[], Awaitable[None]] + ) -> None: + """Handle message processing with retry logic. + + Args: + transaction: The incoming transaction to process + next_handler: The next handler in the pipeline + """ + # Get the current delayed retry attempt count + retry_header = transaction.incoming_message.headers.get(self.RETRY_COUNT_HEADER, "0") + try: + delayed_attempt = int(retry_header) + except ValueError: + delayed_attempt = 0 + + delayed_retry_count = max(1, self.delayed_retry_count) + immediate_retry_count = max(1, self.immediate_retry_count) + + # Attempt immediate retries + for immediate_attempt in range(immediate_retry_count): + try: + await next_handler() + break # Success, exit retry loop + except Exception as error: + # Clear any outgoing messages from failed attempt + transaction.outgoing_messages.clear() + + # If we have more immediate retries left, continue + if immediate_attempt < immediate_retry_count - 1: + continue + + # Check if we can do delayed retries + if delayed_attempt < delayed_retry_count: + # Re-queue the message with a delay + delayed_attempt += 1 + + delayed_message = transaction.add_outgoing_message( + transaction.incoming_message.message, + transaction.bus.name + ) + delayed_message.deliver_at = self.get_next_retry_time(delayed_attempt) + delayed_message.headers[self.RETRY_COUNT_HEADER] = str(delayed_attempt) + + continue + + # All retries exhausted, send to dead letter queue + dead_letter_endpoint = ( + self.dead_letter_endpoint or f"{transaction.bus.name}.Errors" + ) + dead_letter_message = transaction.add_outgoing_message( + transaction.incoming_message.message, + dead_letter_endpoint + ) + dead_letter_message.headers[self.ERROR_MESSAGE_HEADER] = str(error) + dead_letter_message.headers[self.ERROR_STACK_TRACE_HEADER] = ( + self._get_stack_trace() + ) + + def get_next_retry_time(self, attempt: int) -> datetime: + """Calculate the next retry time based on attempt number. + + Args: + attempt: The retry attempt number (1-based) + + Returns: + The datetime when the next retry should occur + """ + return datetime.now(timezone.utc) + timedelta(seconds=attempt * self.delay) + + @staticmethod + def _get_stack_trace() -> str: + """Extract stack trace from an exception. + + Returns: + The stack trace as a string + """ + import traceback + return traceback.format_exc() \ No newline at end of file diff --git a/src/python/src/transport/transaction/message/handlers/incoming_handler.py b/src/python/src/transport/transaction/message/handlers/incoming_handler.py new file mode 100644 index 0000000..c87e2dc --- /dev/null +++ b/src/python/src/transport/transaction/message/handlers/incoming_handler.py @@ -0,0 +1,17 @@ +"""Incoming handler callable type for PolyBus Python implementation.""" + +from typing import Callable, Awaitable +from src.transport.transaction.incoming_transaction import IncomingTransaction + +# Type alias for incoming message handlers +IncomingHandler = Callable[[IncomingTransaction, Callable[[], Awaitable[None]]], Awaitable[None]] +""" +A callable for handling incoming messages from the transport. + +Args: + transaction: The incoming transaction being processed. + next: A callable that represents the next handler in the pipeline. + +Returns: + An awaitable that completes when the handler finishes processing. +""" diff --git a/src/python/src/transport/transaction/message/handlers/outgoing_handler.py b/src/python/src/transport/transaction/message/handlers/outgoing_handler.py new file mode 100644 index 0000000..3790de5 --- /dev/null +++ b/src/python/src/transport/transaction/message/handlers/outgoing_handler.py @@ -0,0 +1,17 @@ +"""Outgoing handler callable type for PolyBus Python implementation.""" + +from typing import Callable, Awaitable +from src.transport.transaction.outgoing_transaction import OutgoingTransaction + +# Type alias for outgoing message handlers +OutgoingHandler = Callable[[OutgoingTransaction, Callable[[], Awaitable[None]]], Awaitable[None]] +""" +A callable for handling outgoing messages to the transport. + +Args: + transaction: The outgoing transaction being processed. + next: A callable that represents the next handler in the pipeline. + +Returns: + An awaitable that completes when the handler finishes processing. +""" diff --git a/src/python/src/transport/transaction/message/handlers/serializers/__init__.py b/src/python/src/transport/transaction/message/handlers/serializers/__init__.py new file mode 100644 index 0000000..4e74086 --- /dev/null +++ b/src/python/src/transport/transaction/message/handlers/serializers/__init__.py @@ -0,0 +1 @@ +"""Serializer handlers for PolyBus messages.""" \ No newline at end of file diff --git a/src/python/src/transport/transaction/message/handlers/serializers/json_handlers.py b/src/python/src/transport/transaction/message/handlers/serializers/json_handlers.py new file mode 100644 index 0000000..293db4f --- /dev/null +++ b/src/python/src/transport/transaction/message/handlers/serializers/json_handlers.py @@ -0,0 +1,105 @@ +"""JSON serialization handlers for PolyBus Python implementation.""" + +import json +from typing import Optional, Callable, Awaitable +from src.headers import Headers +from src.transport.transaction.incoming_transaction import IncomingTransaction +from src.transport.transaction.outgoing_transaction import OutgoingTransaction + + +class JsonHandlers: + """Provides JSON serialization and deserialization handlers for message processing.""" + + def __init__( + self, + json_options: Optional[dict] = None, + content_type: str = "application/json", + throw_on_missing_type: bool = True, + throw_on_invalid_type: bool = True + ): + """Initialize the JSON handlers. + + Args: + json_options: Optional dictionary of options to pass to json.dumps/loads + content_type: The content type header value to set for serialized messages + throw_on_missing_type: Whether to throw an exception when type header is missing/invalid + throw_on_invalid_type: Whether to throw an exception when message type is not registered + """ + self.json_options = json_options or {} + self.content_type = content_type + self.throw_on_missing_type = throw_on_missing_type + self.throw_on_invalid_type = throw_on_invalid_type + + async def deserializer( + self, + transaction: IncomingTransaction, + next_handler: Callable[[], Awaitable[None]] + ) -> None: + """Deserialize incoming message body from JSON. + + Args: + transaction: The incoming transaction containing the message to deserialize + next_handler: The next handler in the pipeline + + Raises: + InvalidOperationError: When type header is missing and throw_on_missing_type is True + """ + message = transaction.incoming_message + + # Try to get the message type from the headers + message_type_header = message.headers.get(Headers.MESSAGE_TYPE) + message_type = None + + if message_type_header: + message_type = message.bus.messages.get_type_by_header(message_type_header) + + if message_type is None and self.throw_on_missing_type: + raise InvalidOperationError( + "The type header is missing, invalid, or if the type cannot be found." + ) + + # Deserialize the message + if message_type is None: + # No type available, parse as generic JSON + message.message = json.loads(message.body, **self.json_options) + else: + # We have a type, but for Python we'll still parse as JSON and let + # the application handle the type conversion + message.message = json.loads(message.body, **self.json_options) + message.message_type = message_type + + await next_handler() + + async def serializer( + self, + transaction: OutgoingTransaction, + next_handler: Callable[[], Awaitable[None]] + ) -> None: + """Serialize outgoing message objects to JSON. + + Args: + transaction: The outgoing transaction containing messages to serialize + next_handler: The next handler in the pipeline + + Raises: + InvalidOperationError: When message type is not registered and throw_on_invalid_type is True + """ + for message in transaction.outgoing_messages: + # Serialize the message to JSON + message.body = json.dumps(message.message, **self.json_options) + message.headers[Headers.CONTENT_TYPE] = self.content_type + + # Set the message type header + header = message.bus.messages.get_header(message.message_type) + + if header is not None: + message.headers[Headers.MESSAGE_TYPE] = header + elif self.throw_on_invalid_type: + raise InvalidOperationError("The header has an invalid type.") + + await next_handler() + + +class InvalidOperationError(Exception): + """Exception raised when an invalid operation is attempted.""" + pass \ No newline at end of file diff --git a/src/python/src/transport/transaction/message/incoming_message.py b/src/python/src/transport/transaction/message/incoming_message.py new file mode 100644 index 0000000..06a101c --- /dev/null +++ b/src/python/src/transport/transaction/message/incoming_message.py @@ -0,0 +1,60 @@ +from typing import Any, Optional, Type +from src.transport.transaction.message.message import Message +from src.i_poly_bus import IPolyBus + + +class IncomingMessage(Message): + """ + Represents an incoming message from the transport. + """ + + def __init__(self, bus: "IPolyBus", body: str, message: Optional[Any] = None, message_type: Optional[Type] = None): + super().__init__(bus) + if body is None: + raise ValueError("body cannot be None") + + self._message_type = message_type or str + self._body = body + self._message = message if message is not None else body + + @property + def message_type(self) -> Type: + """ + The default is string, but can be changed based on deserialization. + """ + return self._message_type + + @message_type.setter + def message_type(self, value: Type) -> None: + """ + Set the message type. + """ + self._message_type = value + + @property + def body(self) -> str: + """ + The message body contents. + """ + return self._body + + @body.setter + def body(self, value: str) -> None: + """ + Set the message body contents. + """ + self._body = value + + @property + def message(self) -> Any: + """ + The deserialized message object, otherwise the same value as Body. + """ + return self._message + + @message.setter + def message(self, value: Any) -> None: + """ + Set the deserialized message object. + """ + self._message = value diff --git a/src/python/src/transport/transaction/message/message.py b/src/python/src/transport/transaction/message/message.py new file mode 100644 index 0000000..689cdcf --- /dev/null +++ b/src/python/src/transport/transaction/message/message.py @@ -0,0 +1,43 @@ +from typing import Dict, Any +from src.i_poly_bus import IPolyBus + + +class Message: + """ + Base message class for PolyBus transport. + """ + + def __init__(self, bus: "IPolyBus"): + if bus is None: + raise ValueError("bus cannot be None") + self._bus = bus + self._state: Dict[str, Any] = {} + self._headers: Dict[str, str] = {} + + @property + def state(self) -> Dict[str, Any]: + """ + State dictionary that can be used to store arbitrary data associated with the message. + """ + return self._state + + @property + def headers(self) -> Dict[str, str]: + """ + Message headers from the transport. + """ + return self._headers + + @headers.setter + def headers(self, value: Dict[str, str]) -> None: + """ + Set message headers from the transport. + """ + self._headers = value + + @property + def bus(self) -> "IPolyBus": + """ + The bus instance associated with the message. + """ + return self._bus diff --git a/src/python/src/transport/transaction/message/message_info.py b/src/python/src/transport/transaction/message/message_info.py new file mode 100644 index 0000000..e9b19fe --- /dev/null +++ b/src/python/src/transport/transaction/message/message_info.py @@ -0,0 +1,132 @@ +import re +from typing import Optional +from src.transport.transaction.message.message_type import MessageType + + +class MessageInfo: + """ + Decorator that adds metadata about a message class. + This is used to identify the message type and version so that it can be routed and deserialized appropriately. + """ + + def __init__(self, message_type: MessageType, endpoint: str, name: str, major: int, minor: int, patch: int): + """ + Initialize MessageInfo decorator. + + Args: + message_type: If the message is a command or event + endpoint: The endpoint that publishes the event message or the endpoint that handles the command + name: The unique name for the message for the given endpoint + major: The major version of the message schema + minor: The minor version of the message schema + patch: The patch version of the message schema + """ + self.message_type = message_type + self.endpoint = endpoint + self.name = name + self.major = major + self.minor = minor + self.patch = patch + + @staticmethod + def get_attribute_from_header(header: str) -> Optional['MessageInfo']: + """ + Parses a message attribute from a message header string. + + Args: + header: The header string to parse + + Returns: + If the header is valid, returns a MessageInfo instance; otherwise, returns None. + """ + pattern = re.compile( + r'^\s*endpoint\s*=\s*(?P[^,\s]+)\s*,\s*type\s*=\s*(?P[^,\s]+)\s*,\s*name\s*=\s*(?P[^,\s]+)\s*,\s*version\s*=\s*(?P\d+)\.(?P\d+)\.(?P\d+)\s*$', + re.IGNORECASE + ) + + match = pattern.match(header) + if not match: + return None + + try: + endpoint = match.group('endpoint') + name = match.group('name') + message_type = MessageType(match.group('type').lower()) + major = int(match.group('major')) + minor = int(match.group('minor')) + patch = int(match.group('patch')) + + return MessageInfo(message_type, endpoint, name, major, minor, patch) + except (ValueError, AttributeError): + return None + + def __eq__(self, other) -> bool: + """ + Compares two message attributes for equality. + The patch and minor versions are not considered for equality. + """ + if not isinstance(other, MessageInfo): + return False + return ( + self.message_type == other.message_type + and self.endpoint == other.endpoint + and self.name == other.name + and self.major == other.major + ) + + def __hash__(self) -> int: + return hash((self.message_type, self.endpoint, self.name, self.major, self.minor, self.patch)) + + def to_string(self, include_version: bool = True) -> str: + """ + Serializes the message attribute to a string format suitable for message headers. + + Args: + include_version: Whether to include version information in the string + + Returns: + String representation suitable for message headers + """ + base = f"endpoint={self.endpoint}, type={self.message_type.value}, name={self.name}" + if include_version: + base += f", version={self.major}.{self.minor}.{self.patch}" + return base + + def __str__(self) -> str: + return self.to_string(True) + + def __call__(self, cls): + """ + Decorator function that attaches message info to a class. + + Args: + cls: The class to decorate + + Returns: + The decorated class with _message_info attribute + """ + cls._message_info = self + return cls + + +def message_info(message_type: MessageType, endpoint: str, name: str, major: int, minor: int, patch: int): + """ + Decorator factory for creating message info decorators. + + Args: + message_type: If the message is a command or event + endpoint: The endpoint that publishes the event message or the endpoint that handles the command + name: The unique name for the message for the given endpoint + major: The major version of the message schema + minor: The minor version of the message schema + patch: The patch version of the message schema + + Returns: + MessageInfo decorator instance + + Example: + @message_info(MessageType.COMMAND, "my-endpoint", "my-name", 1, 2, 3) + class MyModel: + pass + """ + return MessageInfo(message_type, endpoint, name, major, minor, patch) diff --git a/src/python/src/transport/transaction/message/message_type.py b/src/python/src/transport/transaction/message/message_type.py new file mode 100644 index 0000000..e4531ac --- /dev/null +++ b/src/python/src/transport/transaction/message/message_type.py @@ -0,0 +1,19 @@ +from enum import Enum + + +class MessageType(Enum): + """ + Message type enumeration. + """ + + COMMAND = "command" + """ + Command message type. + Commands are messages that are sent to and processed by a single endpoint. + """ + + EVENT = "event" + """ + Event message type. + Events are messages that can be processed by multiple endpoints and sent from a single endpoint. + """ \ No newline at end of file diff --git a/src/python/src/transport/transaction/message/messages.py b/src/python/src/transport/transaction/message/messages.py new file mode 100644 index 0000000..2190676 --- /dev/null +++ b/src/python/src/transport/transaction/message/messages.py @@ -0,0 +1,127 @@ +""" +A collection of message types and their associated message headers. +""" +from threading import Lock +from typing import Dict, Optional, Type, Tuple +import threading +from src.transport.transaction.message.message_info import MessageInfo + + +class Messages: + """ + A collection of message types and their associated message headers. + """ + _lock: Lock + + def __init__(self): + """Initialize the Messages collection.""" + self._map: Dict[str, Optional[Type]] = {} + self._types: Dict[Type, Tuple[MessageInfo, str]] = {} + self._lock = threading.Lock() + + def get_message_info(self, message_type: Type) -> Optional[MessageInfo]: + """ + Gets the message attribute associated with the specified type. + + Args: + message_type: The message type to get the attribute for + + Returns: + The MessageInfo if found, otherwise None + """ + with self._lock: + entry = self._types.get(message_type) + return entry[0] if entry else None + + def get_type_by_header(self, header: str) -> Optional[Type]: + """ + Attempts to get the message type associated with the specified header. + + Args: + header: The message header to look up + + Returns: + If found, returns the message type; otherwise, returns None. + """ + attribute = MessageInfo.get_attribute_from_header(header) + if attribute is None: + return None + + with self._lock: + # Check cache first + if header in self._map: + return self._map[header] + + # Find matching type + for msg_type, (msg_attribute, _) in self._types.items(): + if msg_attribute == attribute: + self._map[header] = msg_type + return msg_type + + # Cache miss result + self._map[header] = None + return None + + def get_header(self, message_type: Type) -> Optional[str]: + """ + Attempts to get the message header associated with the specified type. + + Args: + message_type: The message type to get the header for + + Returns: + If found, returns the message header; otherwise, returns None. + """ + with self._lock: + entry = self._types.get(message_type) + return entry[1] if entry else None + + def add(self, message_type: Type) -> MessageInfo: + """ + Adds a message type to the collection. + The message type must have a MessageInfo decorator applied. + + Args: + message_type: The message type to add + + Returns: + The MessageInfo associated with the message type + + Raises: + ValueError: If the type does not have a MessageInfo decorator + KeyError: If the type is already registered + """ + # Check for MessageInfo attribute + if not hasattr(message_type, '_message_info'): + raise ValueError(f"Type {message_type.__module__}.{message_type.__name__} does not have a MessageInfo decorator.") + + attribute = message_type._message_info + if not isinstance(attribute, MessageInfo): + raise ValueError(f"Type {message_type.__module__}.{message_type.__name__} does not have a valid MessageInfo decorator.") + + header = attribute.to_string(True) + + with self._lock: + if message_type in self._types: + raise KeyError(f"Type {message_type.__module__}.{message_type.__name__} is already registered.") + + self._types[message_type] = (attribute, header) + self._map[header] = message_type + + return attribute + + def get_type_by_message_info(self, message_info: MessageInfo) -> Optional[Type]: + """ + Attempts to get the message type associated with the specified MessageInfo. + + Args: + message_info: The MessageInfo to look up + + Returns: + If found, returns the message type; otherwise, returns None. + """ + with self._lock: + for msg_type, (msg_attribute, _) in self._types.items(): + if msg_attribute == message_info: + return msg_type + return None diff --git a/src/python/src/transport/transaction/message/outgoing_message.py b/src/python/src/transport/transaction/message/outgoing_message.py new file mode 100644 index 0000000..8e365e7 --- /dev/null +++ b/src/python/src/transport/transaction/message/outgoing_message.py @@ -0,0 +1,89 @@ +from typing import Any, Optional, Type +from datetime import datetime +from src.transport.transaction.message.message import Message +from src.i_poly_bus import IPolyBus + + +class OutgoingMessage(Message): + """ + Represents an outgoing message to the transport. + """ + + def __init__(self, bus: "IPolyBus", message: Any, endpoint: str): + super().__init__(bus) + self._message = message + self._message_type = type(message) + self._body = str(message) if message is not None else "" + self._endpoint = endpoint + self._deliver_at: Optional[datetime] = None + + @property + def deliver_at(self) -> Optional[datetime]: + """ + If the transport supports delayed messages, this is the time at which the message should be delivered. + """ + return self._deliver_at + + @deliver_at.setter + def deliver_at(self, value: Optional[datetime]) -> None: + """ + Set the delivery time for delayed messages. + """ + self._deliver_at = value + + @property + def message_type(self) -> Type: + """ + The type of the message. + """ + return self._message_type + + @message_type.setter + def message_type(self, value: Type) -> None: + """ + Set the message type. + """ + self._message_type = value + + @property + def body(self) -> str: + """ + The serialized message body contents. + """ + return self._body + + @body.setter + def body(self, value: str) -> None: + """ + Set the serialized message body contents. + """ + self._body = value + + @property + def endpoint(self) -> str: + """ + If the message is a command then this is the endpoint the message is being sent to. + If the message is an event then this is the source endpoint the message is being sent from. + """ + return self._endpoint + + @endpoint.setter + def endpoint(self, value: str) -> None: + """ + Set the endpoint. + """ + self._endpoint = value + + @property + def message(self) -> Any: + """ + The message object. + """ + return self._message + + @message.setter + def message(self, value: Any) -> None: + """ + Set the message object. + """ + self._message = value diff --git a/src/python/src/transport/transaction/outgoing_transaction.py b/src/python/src/transport/transaction/outgoing_transaction.py new file mode 100644 index 0000000..72fa30b --- /dev/null +++ b/src/python/src/transport/transaction/outgoing_transaction.py @@ -0,0 +1,16 @@ +"""Outgoing transaction class for PolyBus Python implementation.""" + +from src.transport.transaction.transaction import Transaction +from src.i_poly_bus import IPolyBus + + +class OutgoingTransaction(Transaction): + """Represents an outgoing transaction in the PolyBus system.""" + + def __init__(self, bus: IPolyBus): + """Initialize an outgoing transaction. + + Args: + bus: The PolyBus instance associated with this transaction. + """ + super().__init__(bus) diff --git a/src/python/src/transport/transaction/transaction.py b/src/python/src/transport/transaction/transaction.py new file mode 100644 index 0000000..699aaa1 --- /dev/null +++ b/src/python/src/transport/transaction/transaction.py @@ -0,0 +1,75 @@ +"""Base transaction class for PolyBus Python implementation.""" + +from typing import Dict, List, Any, Optional +from abc import ABC +from src.transport.transaction.message.outgoing_message import OutgoingMessage +from src.i_poly_bus import IPolyBus + + +class Transaction(ABC): + """Base class for transactions in the PolyBus system.""" + + def __init__(self, bus: IPolyBus): + """Initialize a transaction. + + Args: + bus: The PolyBus instance associated with this transaction. + + Raises: + ValueError: If bus is None. + """ + if bus is None: + raise ValueError("bus cannot be None") + self._bus = bus + self._state: Dict[str, Any] = {} + self._outgoing_messages: List[OutgoingMessage] = [] + + @property + def bus(self) -> IPolyBus: + """The bus instance associated with the transaction.""" + return self._bus + + @property + def state(self) -> Dict[str, Any]: + """State dictionary that can be used to store arbitrary data associated with the transaction.""" + return self._state + + @property + def outgoing_messages(self) -> List[OutgoingMessage]: + """A list of outgoing messages to be sent when the transaction is committed.""" + return self._outgoing_messages + + def add_outgoing_message(self, message: Any, endpoint: Optional[str] = None) -> OutgoingMessage: + """Add an outgoing message to this transaction. + + Args: + message: The message object to send. + endpoint: Optional endpoint override. If not provided, will be determined from message type. + + Returns: + The created OutgoingMessage instance. + + Raises: + ValueError: If message type is not registered. + """ + def get_endpoint() -> str: + message_info = self._bus.messages.get_message_info(type(message)) + if message_info is None: + raise ValueError(f"Message type {type(message).__name__} is not registered.") + return message_info.endpoint + + outgoing_message = OutgoingMessage( + self._bus, + message, + endpoint if endpoint is not None else get_endpoint() + ) + self._outgoing_messages.append(outgoing_message) + return outgoing_message + + async def abort(self) -> None: + """If an exception occurs during processing, the transaction will be aborted.""" + pass + + async def commit(self) -> None: + """If no exception occurs during processing, the transaction will be committed.""" + await self._bus.send(self) diff --git a/src/python/src/transport/transaction/transaction_factory.py b/src/python/src/transport/transaction/transaction_factory.py new file mode 100644 index 0000000..9b0a7b6 --- /dev/null +++ b/src/python/src/transport/transaction/transaction_factory.py @@ -0,0 +1,22 @@ +"""Transaction factory for creating transactions in the PolyBus Python implementation.""" + +from typing import Callable, Optional, Awaitable +from src.transport.transaction import Transaction + +TransactionFactory = Callable[ + ['PolyBusBuilder', 'IPolyBus', Optional['IncomingMessage']], + Awaitable['Transaction'] +] +""" +A callable for creating a new transaction for processing a request. +This should be used to integrate with external transaction systems to ensure message processing +is done within the context of a transaction. + +Args: + builder: The PolyBus builder instance. + bus: The PolyBus instance. + message: The incoming message to process, if any. + +Returns: + An awaitable that resolves to a Transaction instance for processing the request. +""" diff --git a/src/python/src/transport/transport_factory.py b/src/python/src/transport/transport_factory.py new file mode 100644 index 0000000..c2bba59 --- /dev/null +++ b/src/python/src/transport/transport_factory.py @@ -0,0 +1,8 @@ +"""Transport factory for the PolyBus Python implementation.""" + +from typing import Callable, Awaitable +from src.transport.i_transport import ITransport + +# Type alias for transport factory function +# Creates a transport instance to be used by PolyBus +TransportFactory = Callable[['PolyBusBuilder', 'IPolyBus'], Awaitable['ITransport']] \ No newline at end of file diff --git a/src/python/tests/__init__.py b/src/python/tests/__init__.py new file mode 100644 index 0000000..5e1a8a5 --- /dev/null +++ b/src/python/tests/__init__.py @@ -0,0 +1 @@ +# This file makes the tests directory a Python package \ No newline at end of file diff --git a/src/python/tests/test_poly_bus.py b/src/python/tests/test_poly_bus.py new file mode 100644 index 0000000..6aaef11 --- /dev/null +++ b/src/python/tests/test_poly_bus.py @@ -0,0 +1,405 @@ +"""Tests for the PolyBus class. + +This test suite mirrors the functionality of the C# PolyBus tests, covering: + +1. Incoming message handlers: + - Basic handler invocation + - Handler invocation with delayed messages + - Handler invocation with exceptions (both with and without delays) + +2. Outgoing message handlers: + - Basic handler invocation + - Handler invocation with exceptions + +3. Bus functionality: + - Property access and configuration + - Transaction creation (both incoming and outgoing) + - Handler chain execution order + - Start/stop operations + - Exception handling and transaction abort + +These tests use mock objects to simulate the transport layer and message handling +without requiring the full infrastructure, making them fast and reliable for unit testing. +""" + +import asyncio +import pytest +from datetime import datetime, timedelta + +# Import the classes we need to test +from src.poly_bus_builder import PolyBusBuilder +from src.transport.transaction.incoming_transaction import IncomingTransaction +from src.transport.transaction.outgoing_transaction import OutgoingTransaction +from src.transport.transaction.message.incoming_message import IncomingMessage + + +class MockIncomingMessage(IncomingMessage): + """Mock incoming message for testing.""" + def __init__(self, bus, body): + super().__init__(bus, body) + + +class MockIncomingTransaction(IncomingTransaction): + """Mock incoming transaction for testing.""" + def __init__(self, bus, incoming_message): + # Initialize the parent IncomingTransaction class properly + super().__init__(bus, incoming_message) + + async def abort(self): + pass + + +class MockOutgoingTransaction(OutgoingTransaction): + """Mock outgoing transaction for testing.""" + def __init__(self, bus): + # Initialize the parent classes properly + super().__init__(bus) + + def add_outgoing_message(self, message, endpoint): + outgoing_message = MockOutgoingMessage(self.bus, message, endpoint) + self.outgoing_messages.append(outgoing_message) + return outgoing_message + + async def abort(self): + pass + + async def commit(self): + await self.bus.send(self) + + +class MockOutgoingMessage: + """Mock outgoing message for testing.""" + def __init__(self, bus, message, endpoint): + self.bus = bus + self.body = str(message) + self.endpoint = endpoint + self.deliver_at = None + self.headers = {} # Add headers attribute + self.message_type = "test" # Add message_type attribute + + +async def custom_transaction_factory(builder, bus, message=None): + """Custom transaction factory for testing.""" + if message is not None: + return MockIncomingTransaction(bus, message) + else: + return MockOutgoingTransaction(bus) + + +class TestPolyBus: + """Test suite for the PolyBus class.""" + + @pytest.mark.asyncio + async def test_incoming_handlers_is_invoked(self): + """Test that incoming handlers are invoked when processing messages.""" + # Arrange + incoming_transaction_future = asyncio.Future() + + async def incoming_handler(transaction, next_handler): + await next_handler() + incoming_transaction_future.set_result(transaction) + + builder = PolyBusBuilder() + builder.transaction_factory = custom_transaction_factory + builder.incoming_handlers.append(incoming_handler) + bus = await builder.build() + + # Act + await bus.start() + + # Create an incoming message and transaction to simulate receiving a message + incoming_message = MockIncomingMessage(bus, "Hello world") + incoming_transaction = MockIncomingTransaction(bus, incoming_message) + await bus.send(incoming_transaction) + + transaction = await incoming_transaction_future + await bus.stop() + + # Assert + assert transaction.incoming_message.body == "Hello world" + + @pytest.mark.asyncio + async def test_incoming_handlers_with_delay_is_invoked(self): + """Test that incoming handlers are invoked when processing delayed messages.""" + # Arrange + processed_on_future = asyncio.Future() + + async def incoming_handler(transaction, next_handler): + await next_handler() + processed_on_future.set_result(datetime.utcnow()) + + builder = PolyBusBuilder() + builder.transaction_factory = custom_transaction_factory + builder.incoming_handlers.append(incoming_handler) + bus = await builder.build() + + # Act + await bus.start() + outgoing_transaction = await bus.create_transaction() + message = outgoing_transaction.add_outgoing_message("Hello world", "unknown-endpoint") + scheduled_at = datetime.utcnow() + timedelta(seconds=5) + message.deliver_at = scheduled_at + + # Simulate delayed processing + await asyncio.sleep(0.01) # Small delay to simulate processing time + incoming_message = MockIncomingMessage(bus, "Hello world") + incoming_transaction = MockIncomingTransaction(bus, incoming_message) + await bus.send(incoming_transaction) + + processed_on = await processed_on_future + await bus.stop() + + # Assert + # In this test, we're not actually implementing delay functionality, + # but we're testing that the handler gets called + assert processed_on is not None + assert isinstance(processed_on, datetime) + + @pytest.mark.asyncio + async def test_incoming_handlers_with_delay_and_exception_is_invoked(self): + """Test that incoming handlers are invoked even when exceptions occur during delayed processing.""" + # Arrange + processed_on_future = asyncio.Future() + + async def incoming_handler(transaction, next_handler): + processed_on_future.set_result(datetime.utcnow()) + raise Exception(transaction.incoming_message.body) + + builder = PolyBusBuilder() + builder.transaction_factory = custom_transaction_factory + builder.incoming_handlers.append(incoming_handler) + bus = await builder.build() + + # Act + await bus.start() + outgoing_transaction = await bus.create_transaction() + message = outgoing_transaction.add_outgoing_message("Hello world", "unknown-endpoint") + scheduled_at = datetime.utcnow() + timedelta(seconds=5) + message.deliver_at = scheduled_at + + # Simulate processing with exception + incoming_message = MockIncomingMessage(bus, "Hello world") + incoming_transaction = MockIncomingTransaction(bus, incoming_message) + + with pytest.raises(Exception) as exc_info: + await bus.send(incoming_transaction) + + processed_on = await processed_on_future + await bus.stop() + + # Assert + assert processed_on is not None + assert str(exc_info.value) == "Hello world" + + @pytest.mark.asyncio + async def test_incoming_handlers_with_exception_is_invoked(self): + """Test that incoming handlers are invoked and exceptions are properly handled.""" + # Arrange + incoming_transaction_future = asyncio.Future() + + def incoming_handler(transaction, next_handler): + incoming_transaction_future.set_result(transaction) + raise Exception(transaction.incoming_message.body) + + builder = PolyBusBuilder() + builder.transaction_factory = custom_transaction_factory + builder.incoming_handlers.append(incoming_handler) + bus = await builder.build() + + # Act + await bus.start() + + incoming_message = MockIncomingMessage(bus, "Hello world") + incoming_transaction = MockIncomingTransaction(bus, incoming_message) + + with pytest.raises(Exception) as exc_info: + await bus.send(incoming_transaction) + + transaction = await incoming_transaction_future + await bus.stop() + + # Assert + assert transaction.incoming_message.body == "Hello world" + assert str(exc_info.value) == "Hello world" + + @pytest.mark.asyncio + async def test_outgoing_handlers_is_invoked(self): + """Test that outgoing handlers are invoked when processing outgoing messages.""" + # Arrange + outgoing_transaction_future = asyncio.Future() + + async def outgoing_handler(transaction, next_handler): + await next_handler() + outgoing_transaction_future.set_result(transaction) + + builder = PolyBusBuilder() + builder.transaction_factory = custom_transaction_factory + builder.outgoing_handlers.append(outgoing_handler) + bus = await builder.build() + + # Act + await bus.start() + outgoing_transaction = await bus.create_transaction() + outgoing_transaction.add_outgoing_message("Hello world", "unknown-endpoint") + await outgoing_transaction.commit() + + transaction = await outgoing_transaction_future + await bus.stop() + + # Assert + assert len(transaction.outgoing_messages) == 1 + assert transaction.outgoing_messages[0].body == "Hello world" + + @pytest.mark.asyncio + async def test_outgoing_handlers_with_exception_is_invoked(self): + """Test that outgoing handlers are invoked and exceptions are properly handled.""" + # Arrange + def outgoing_handler(transaction, next_handler): + raise Exception(transaction.outgoing_messages[0].body) + + builder = PolyBusBuilder() + builder.transaction_factory = custom_transaction_factory + builder.outgoing_handlers.append(outgoing_handler) + bus = await builder.build() + + # Act + await bus.start() + outgoing_transaction = await bus.create_transaction() + outgoing_transaction.add_outgoing_message("Hello world", "unknown-endpoint") + + with pytest.raises(Exception) as exc_info: + await outgoing_transaction.commit() + + await bus.stop() + + # Assert + assert str(exc_info.value) == "Hello world" + + @pytest.mark.asyncio + async def test_bus_properties_are_accessible(self): + """Test that bus properties are accessible and properly configured.""" + # Arrange + builder = PolyBusBuilder() + builder.properties["test_key"] = "test_value" + builder.name = "TestBus" + bus = await builder.build() + + # Assert + assert bus.properties["test_key"] == "test_value" + assert bus.name == "TestBus" + assert bus.transport is not None + assert bus.incoming_handlers == builder.incoming_handlers + assert bus.outgoing_handlers == builder.outgoing_handlers + assert bus.messages == builder.messages + + @pytest.mark.asyncio + async def test_create_transaction_without_message(self): + """Test creating an outgoing transaction without a message.""" + # Arrange + builder = PolyBusBuilder() + bus = await builder.build() + + # Act + transaction = await bus.create_transaction() + + # Assert + assert transaction is not None + assert transaction.bus == bus + assert hasattr(transaction, 'outgoing_messages') + + @pytest.mark.asyncio + async def test_create_transaction_with_message(self): + """Test creating an incoming transaction with a message.""" + # Arrange + builder = PolyBusBuilder() + builder.transaction_factory = custom_transaction_factory + bus = await builder.build() + incoming_message = MockIncomingMessage(bus, "Test message") + + # Act + transaction = await bus.create_transaction(incoming_message) + + # Assert + assert transaction is not None + assert transaction.bus == bus + assert hasattr(transaction, 'incoming_message') + + @pytest.mark.asyncio + async def test_handler_chain_execution_order(self): + """Test that handlers are executed in the correct order.""" + # Arrange + execution_order = [] + + async def handler1(transaction, next_handler): + execution_order.append("handler1_start") + await next_handler() + execution_order.append("handler1_end") + + async def handler2(transaction, next_handler): + execution_order.append("handler2_start") + await next_handler() + execution_order.append("handler2_end") + + builder = PolyBusBuilder() + builder.transaction_factory = custom_transaction_factory + builder.outgoing_handlers.extend([handler1, handler2]) + bus = await builder.build() + + # Act + await bus.start() + outgoing_transaction = await bus.create_transaction() + outgoing_transaction.add_outgoing_message("Test message", "test-endpoint") + await outgoing_transaction.commit() + await bus.stop() + + # Assert + # Handlers execute in order, but nested (like middleware) + # handler1 starts, calls next (handler2), handler2 completes, then handler1 completes + expected_order = ["handler1_start", "handler2_start", "handler2_end", "handler1_end"] + assert execution_order == expected_order + + @pytest.mark.asyncio + async def test_bus_start_and_stop(self): + """Test that bus can be started and stopped properly.""" + # Arrange + builder = PolyBusBuilder() + bus = await builder.build() + + # Act & Assert + await bus.start() # Should not raise an exception + await bus.stop() # Should not raise an exception + + @pytest.mark.asyncio + async def test_transaction_abort_on_exception(self): + """Test that transaction.abort() is called when an exception occurs.""" + # Arrange + abort_called = asyncio.Future() + + class MockTransactionWithAbort(OutgoingTransaction): + def __init__(self, bus): + super().__init__(bus) + + async def abort(self): + abort_called.set_result(True) + + async def mock_transaction_factory(builder, bus, message=None): + return MockTransactionWithAbort(bus) + + async def failing_handler(transaction, next_handler): + raise Exception("Test exception") + + builder = PolyBusBuilder() + builder.transaction_factory = mock_transaction_factory + builder.outgoing_handlers.append(failing_handler) + bus = await builder.build() + + # Act + await bus.start() + transaction = await bus.create_transaction() + + with pytest.raises(Exception): + await bus.send(transaction) + + # Assert + assert await abort_called == True + await bus.stop() \ No newline at end of file diff --git a/src/python/tests/transport/__init__.py b/src/python/tests/transport/__init__.py new file mode 100644 index 0000000..6165323 --- /dev/null +++ b/src/python/tests/transport/__init__.py @@ -0,0 +1 @@ +# Test module for transport \ No newline at end of file diff --git a/src/python/tests/transport/in_memory/__init__.py b/src/python/tests/transport/in_memory/__init__.py new file mode 100644 index 0000000..6b9c0de --- /dev/null +++ b/src/python/tests/transport/in_memory/__init__.py @@ -0,0 +1 @@ +"""In-memory transport tests.""" \ No newline at end of file diff --git a/src/python/tests/transport/in_memory/test_in_memory_transport.py b/src/python/tests/transport/in_memory/test_in_memory_transport.py new file mode 100644 index 0000000..57446f4 --- /dev/null +++ b/src/python/tests/transport/in_memory/test_in_memory_transport.py @@ -0,0 +1,89 @@ +"""Tests for InMemoryTransport - Python equivalent of C# InMemoryTests. + +This test suite mirrors the C# InMemoryTests.cs functionality using the actual +PolyBus implementation components (not mocks). + +The tests cover: +1. Message flow with subscription filtering (equivalent to C# InMemory_WithSubscription test) + +Key differences from C# version: +- Uses async/await patterns instead of Task-based async +- Uses pytest fixtures and markers for async testing +- Follows Python naming conventions (snake_case vs PascalCase) +""" + +import pytest +import asyncio +from src.poly_bus_builder import PolyBusBuilder +from src.transport.in_memory.in_memory_transport import InMemoryTransport +from src.transport.transaction.message.message_info import MessageInfo +from src.transport.transaction.message.message_type import MessageType +from src.headers import Headers + + +@MessageInfo(MessageType.COMMAND, "test-service", "TestMessage", 1, 0, 0) +class SampleMessage: + """Test message class decorated with MessageInfo.""" + + def __init__(self, name: str = ""): + self.name = name + + def __str__(self): + return self.name + + +class TestInMemoryTransport: + """Test cases for InMemoryTransport that mirror C# InMemoryTests.""" + + @pytest.mark.asyncio + async def test_in_memory_with_subscription(self): + """Test InMemoryTransport with subscription - mirrors C# InMemory_WithSubscription test. + + This test validates the complete message flow through InMemoryTransport + when subscriptions are enabled, matching the C# test behavior exactly. + """ + # Arrange + in_memory_transport = InMemoryTransport() + in_memory_transport.use_subscriptions = True + + incoming_transaction_future = asyncio.Future() + + async def incoming_handler(transaction, next_step): + """Incoming handler that captures the transaction.""" + incoming_transaction_future.set_result(transaction) + await next_step() + + async def transport_factory(b, bus): + return in_memory_transport.add_endpoint(b, bus) + + builder = PolyBusBuilder() + builder.incoming_handlers.append(incoming_handler) + builder.transport_factory = transport_factory + builder.messages.add(SampleMessage) + + bus = await builder.build() + + # Get message info from the SampleMessage class + message_info = SampleMessage._message_info + + # Subscribe to the message type + await bus.transport.subscribe(message_info) + + # Act + await bus.start() + outgoing_transaction = await bus.create_transaction() + outgoing_message = outgoing_transaction.add_outgoing_message(SampleMessage(name="TestMessage")) + outgoing_message.headers[Headers.MESSAGE_TYPE] = message_info.to_string(True) + await outgoing_transaction.commit() + + # Wait for incoming transaction (equivalent to TaskCompletionSource in C#) + incoming_transaction = await asyncio.wait_for(incoming_transaction_future, timeout=1.0) + + # Allow async processing to complete + await asyncio.sleep(0.01) + + await bus.stop() + + # Assert + assert incoming_transaction.incoming_message.body == "TestMessage" + assert incoming_transaction.incoming_message.headers[Headers.MESSAGE_TYPE] == message_info.to_string(True) \ No newline at end of file diff --git a/src/python/tests/transport/transaction/__init__.py b/src/python/tests/transport/transaction/__init__.py new file mode 100644 index 0000000..5d3a696 --- /dev/null +++ b/src/python/tests/transport/transaction/__init__.py @@ -0,0 +1 @@ +# Test module for transaction \ No newline at end of file diff --git a/src/python/tests/transport/transaction/message/__init__.py b/src/python/tests/transport/transaction/message/__init__.py new file mode 100644 index 0000000..d594cad --- /dev/null +++ b/src/python/tests/transport/transaction/message/__init__.py @@ -0,0 +1 @@ +# Test module for message \ No newline at end of file diff --git a/src/python/tests/transport/transaction/message/handlers/__init__.py b/src/python/tests/transport/transaction/message/handlers/__init__.py new file mode 100644 index 0000000..136a9f2 --- /dev/null +++ b/src/python/tests/transport/transaction/message/handlers/__init__.py @@ -0,0 +1 @@ +# Test module for handlers \ No newline at end of file diff --git a/src/python/tests/transport/transaction/message/handlers/error/__init__.py b/src/python/tests/transport/transaction/message/handlers/error/__init__.py new file mode 100644 index 0000000..4be09db --- /dev/null +++ b/src/python/tests/transport/transaction/message/handlers/error/__init__.py @@ -0,0 +1 @@ +# Test module for error handlers \ No newline at end of file diff --git a/src/python/tests/transport/transaction/message/handlers/error/test_error_handlers.py b/src/python/tests/transport/transaction/message/handlers/error/test_error_handlers.py new file mode 100644 index 0000000..48c8805 --- /dev/null +++ b/src/python/tests/transport/transaction/message/handlers/error/test_error_handlers.py @@ -0,0 +1,399 @@ +"""Tests for error handling with retry logic for PolyBus Python implementation.""" + +import pytest +from datetime import datetime, timedelta, timezone +from unittest.mock import Mock +from typing import List, Dict, Any, Optional + +from src.transport.transaction.message.handlers.error.error_handlers import ErrorHandler +from src.transport.transaction.incoming_transaction import IncomingTransaction +from src.transport.transaction.message.incoming_message import IncomingMessage +from src.transport.transaction.transaction import Transaction +from src.transport.transaction.outgoing_transaction import OutgoingTransaction + + +class MockableErrorHandler(ErrorHandler): + """Mockable version of ErrorHandler with overridable retry time.""" + + def __init__(self, delay: int = 30, delayed_retry_count: int = 3, + immediate_retry_count: int = 3, dead_letter_endpoint: Optional[str] = None): + super().__init__(delay, delayed_retry_count, immediate_retry_count, dead_letter_endpoint) + self._next_retry_time: Optional[datetime] = None + + def set_next_retry_time(self, retry_time: datetime) -> None: + """Set a specific retry time for testing purposes.""" + self._next_retry_time = retry_time + + def get_next_retry_time(self, attempt: int) -> datetime: + """Return the set retry time or use the default implementation.""" + if self._next_retry_time is not None: + return self._next_retry_time + return super().get_next_retry_time(attempt) + + +class MockTransport: + """Mock implementation of ITransport.""" + + @property + def supports_command_messages(self) -> bool: + return True + + @property + def supports_delayed_messages(self) -> bool: + return True + + @property + def supports_subscriptions(self) -> bool: + return False + + async def send(self, transaction: Transaction) -> None: + pass + + async def subscribe(self, message_info) -> None: + pass + + async def start(self) -> None: + pass + + async def stop(self) -> None: + pass + + +class MockBus: + """Mock implementation of IPolyBus.""" + + def __init__(self, name: str): + self._name = name + self._transport = MockTransport() + self._incoming_handlers = [] + self._outgoing_handlers = [] + self._messages = Mock() + self._properties = {} + + @property + def properties(self) -> Dict[str, Any]: + return self._properties + + @property + def transport(self) -> MockTransport: + return self._transport + + @property + def incoming_handlers(self) -> List: + return self._incoming_handlers + + @property + def outgoing_handlers(self) -> List: + return self._outgoing_handlers + + @property + def messages(self) -> Mock: + return self._messages + + @property + def name(self) -> str: + return self._name + + async def create_transaction(self, message: Optional[IncomingMessage] = None) -> Transaction: + if message is None: + return OutgoingTransaction(self) + return IncomingTransaction(self, message) + + async def send(self, transaction: Transaction) -> None: + pass + + async def start(self) -> None: + pass + + async def stop(self) -> None: + pass + + +class ExceptionWithNullStackTrace(Exception): + """Custom exception that returns None for __traceback__.""" + + def __init__(self, message: str): + super().__init__(message) + # Simulate an exception without stack trace + self.__traceback__ = None + + +@pytest.fixture +def test_bus(): + """Create a test bus instance.""" + return MockBus("TestBus") + + +@pytest.fixture +def incoming_message(test_bus): + """Create a test incoming message.""" + return IncomingMessage(test_bus, "test message body") + + +@pytest.fixture +def transaction(test_bus, incoming_message): + """Create a test transaction.""" + return IncomingTransaction(test_bus, incoming_message) + + +@pytest.fixture +def error_handler(): + """Create a testable error handler.""" + return MockableErrorHandler() + + +class TestErrorHandler: + """Test cases for ErrorHandler class.""" + + @pytest.mark.asyncio + async def test_retrier_succeeds_on_first_attempt_does_not_retry(self, transaction, error_handler): + """Test that successful execution on first attempt doesn't retry.""" + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + await error_handler.retrier(transaction, next_handler) + + assert next_called is True + assert len(transaction.outgoing_messages) == 0 + + @pytest.mark.asyncio + async def test_retrier_fails_once_retries_immediately(self, transaction, error_handler): + """Test that failure retries immediately and succeeds.""" + call_count = 0 + + async def next_handler(): + nonlocal call_count + call_count += 1 + if call_count == 1: + raise Exception("Test error") + + await error_handler.retrier(transaction, next_handler) + + assert call_count == 2 + assert len(transaction.outgoing_messages) == 0 + + @pytest.mark.asyncio + async def test_retrier_fails_all_immediate_retries_schedules_delayed_retry(self, transaction, error_handler): + """Test that failing all immediate retries schedules a delayed retry.""" + expected_retry_time = datetime.utcnow() + timedelta(minutes=5) + error_handler.set_next_retry_time(expected_retry_time) + + call_count = 0 + + async def next_handler(): + nonlocal call_count + call_count += 1 + raise Exception("Test error") + + await error_handler.retrier(transaction, next_handler) + + assert call_count == error_handler.immediate_retry_count + assert len(transaction.outgoing_messages) == 1 + + delayed_message = transaction.outgoing_messages[0] + assert delayed_message.deliver_at == expected_retry_time + assert delayed_message.headers[ErrorHandler.RETRY_COUNT_HEADER] == "1" + assert delayed_message.endpoint == "TestBus" + + @pytest.mark.asyncio + async def test_retrier_with_existing_retry_count_increments_correctly(self, transaction, error_handler): + """Test that existing retry count is incremented correctly.""" + transaction.incoming_message.headers[ErrorHandler.RETRY_COUNT_HEADER] = "2" + expected_retry_time = datetime.utcnow() + timedelta(minutes=10) + error_handler.set_next_retry_time(expected_retry_time) + + async def next_handler(): + raise Exception("Test error") + + await error_handler.retrier(transaction, next_handler) + + assert len(transaction.outgoing_messages) == 1 + + delayed_message = transaction.outgoing_messages[0] + assert delayed_message.headers[ErrorHandler.RETRY_COUNT_HEADER] == "3" + assert delayed_message.deliver_at == expected_retry_time + + @pytest.mark.asyncio + async def test_retrier_exceeds_max_delayed_retries_sends_to_dead_letter(self, transaction, error_handler): + """Test that exceeding max delayed retries sends to dead letter queue.""" + transaction.incoming_message.headers[ErrorHandler.RETRY_COUNT_HEADER] = str(error_handler.delayed_retry_count) + + test_exception = Exception("Final error") + + async def next_handler(): + raise test_exception + + await error_handler.retrier(transaction, next_handler) + + assert len(transaction.outgoing_messages) == 1 + + dead_letter_message = transaction.outgoing_messages[0] + assert dead_letter_message.endpoint == "TestBus.Errors" + assert dead_letter_message.headers[ErrorHandler.ERROR_MESSAGE_HEADER] == "Final error" + assert dead_letter_message.headers[ErrorHandler.ERROR_STACK_TRACE_HEADER] is not None + + @pytest.mark.asyncio + async def test_retrier_with_custom_dead_letter_endpoint_uses_custom_endpoint(self, transaction): + """Test that custom dead letter endpoint is used.""" + error_handler = MockableErrorHandler(dead_letter_endpoint="CustomDeadLetter") + transaction.incoming_message.headers[ErrorHandler.RETRY_COUNT_HEADER] = str(error_handler.delayed_retry_count) + + async def next_handler(): + raise Exception("Final error") + + await error_handler.retrier(transaction, next_handler) + + assert len(transaction.outgoing_messages) == 1 + + dead_letter_message = transaction.outgoing_messages[0] + assert dead_letter_message.endpoint == "CustomDeadLetter" + + @pytest.mark.asyncio + async def test_retrier_clears_outgoing_messages_on_each_retry(self, transaction, error_handler): + """Test that outgoing messages are cleared on each retry attempt.""" + call_count = 0 + + async def next_handler(): + nonlocal call_count + call_count += 1 + transaction.add_outgoing_message("some message", "some endpoint") + raise Exception("Test error") + + await error_handler.retrier(transaction, next_handler) + + assert call_count == error_handler.immediate_retry_count + # Should only have the delayed retry message, not the messages added in next_handler + assert len(transaction.outgoing_messages) == 1 + assert ErrorHandler.RETRY_COUNT_HEADER in transaction.outgoing_messages[0].headers + + @pytest.mark.asyncio + async def test_retrier_with_zero_immediate_retries_schedules_delayed_retry_immediately(self, transaction): + """Test that zero immediate retries still enforces minimum of 1.""" + error_handler = MockableErrorHandler(immediate_retry_count=0) + expected_retry_time = datetime.utcnow() + timedelta(minutes=5) + error_handler.set_next_retry_time(expected_retry_time) + + call_count = 0 + + async def next_handler(): + nonlocal call_count + call_count += 1 + raise Exception("Test error") + + await error_handler.retrier(transaction, next_handler) + + assert call_count == 1 # Should enforce minimum of 1 + assert len(transaction.outgoing_messages) == 1 + assert transaction.outgoing_messages[0].headers[ErrorHandler.RETRY_COUNT_HEADER] == "1" + + @pytest.mark.asyncio + async def test_retrier_with_zero_delayed_retries_still_gets_minimum_of_one(self, transaction): + """Test that zero delayed retries still enforces minimum of 1.""" + error_handler = MockableErrorHandler(delayed_retry_count=0) + expected_retry_time = datetime.utcnow() + timedelta(minutes=5) + error_handler.set_next_retry_time(expected_retry_time) + + async def next_handler(): + raise Exception("Test error") + + await error_handler.retrier(transaction, next_handler) + + # Even with delayed_retry_count = 0, max(1, delayed_retry_count) makes it 1 + assert len(transaction.outgoing_messages) == 1 + assert transaction.outgoing_messages[0].headers[ErrorHandler.RETRY_COUNT_HEADER] == "1" + assert transaction.outgoing_messages[0].deliver_at == expected_retry_time + + def test_get_next_retry_time_default_implementation_uses_delay_correctly(self): + """Test that GetNextRetryTime uses delay correctly.""" + handler = ErrorHandler(delay=60) + before_time = datetime.now(timezone.utc) + + result1 = handler.get_next_retry_time(1) + result2 = handler.get_next_retry_time(2) + result3 = handler.get_next_retry_time(3) + + after_time = datetime.now(timezone.utc) + + assert result1 >= before_time + timedelta(seconds=60) + assert result1 <= after_time + timedelta(seconds=60) + + assert result2 >= before_time + timedelta(seconds=120) + assert result2 <= after_time + timedelta(seconds=120) + + assert result3 >= before_time + timedelta(seconds=180) + assert result3 <= after_time + timedelta(seconds=180) + + @pytest.mark.asyncio + async def test_retrier_succeeds_after_some_immediate_retries_stops_retrying(self, transaction, error_handler): + """Test that success after some immediate retries stops retrying.""" + call_count = 0 + + async def next_handler(): + nonlocal call_count + call_count += 1 + if call_count < 3: # Fail first 2 attempts + raise Exception("Test error") + + await error_handler.retrier(transaction, next_handler) + + assert call_count == 3 + assert len(transaction.outgoing_messages) == 0 + + @pytest.mark.asyncio + async def test_retrier_invalid_retry_count_header_treats_as_zero(self, transaction, error_handler): + """Test that invalid retry count header is treated as zero.""" + transaction.incoming_message.headers[ErrorHandler.RETRY_COUNT_HEADER] = "invalid" + expected_retry_time = datetime.utcnow() + timedelta(minutes=5) + error_handler.set_next_retry_time(expected_retry_time) + + async def next_handler(): + raise Exception("Test error") + + await error_handler.retrier(transaction, next_handler) + + assert len(transaction.outgoing_messages) == 1 + delayed_message = transaction.outgoing_messages[0] + assert delayed_message.headers[ErrorHandler.RETRY_COUNT_HEADER] == "1" + + @pytest.mark.asyncio + async def test_retrier_exception_stack_trace_is_stored_in_header(self, transaction, error_handler): + """Test that exception stack trace is stored in header.""" + transaction.incoming_message.headers[ErrorHandler.RETRY_COUNT_HEADER] = str(error_handler.delayed_retry_count) + + exception_with_stack_trace = Exception("Error with stack trace") + + async def next_handler(): + raise exception_with_stack_trace + + await error_handler.retrier(transaction, next_handler) + + assert len(transaction.outgoing_messages) == 1 + dead_letter_message = transaction.outgoing_messages[0] + assert dead_letter_message.headers[ErrorHandler.ERROR_STACK_TRACE_HEADER] is not None + assert dead_letter_message.headers[ErrorHandler.ERROR_STACK_TRACE_HEADER] != "" + + @pytest.mark.asyncio + async def test_retrier_exception_with_null_stack_trace_uses_empty_string(self, transaction, error_handler): + """Test that exception with null stack trace uses empty string.""" + transaction.incoming_message.headers[ErrorHandler.RETRY_COUNT_HEADER] = str(error_handler.delayed_retry_count) + + # Create an exception with null stack trace + exception_without_stack_trace = ExceptionWithNullStackTrace("Error without stack trace") + + async def next_handler(): + raise exception_without_stack_trace + + await error_handler.retrier(transaction, next_handler) + + assert len(transaction.outgoing_messages) == 1 + dead_letter_message = transaction.outgoing_messages[0] + # In Python, even exceptions without stack trace will have some trace info + # so we just check that the header exists + assert ErrorHandler.ERROR_STACK_TRACE_HEADER in dead_letter_message.headers + + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file diff --git a/src/python/tests/transport/transaction/message/handlers/serializers/__init__.py b/src/python/tests/transport/transaction/message/handlers/serializers/__init__.py new file mode 100644 index 0000000..54c01e1 --- /dev/null +++ b/src/python/tests/transport/transaction/message/handlers/serializers/__init__.py @@ -0,0 +1 @@ +"""Handlers serializers package.""" \ No newline at end of file diff --git a/src/python/tests/transport/transaction/message/handlers/serializers/test_json_handlers.py b/src/python/tests/transport/transaction/message/handlers/serializers/test_json_handlers.py new file mode 100644 index 0000000..981c664 --- /dev/null +++ b/src/python/tests/transport/transaction/message/handlers/serializers/test_json_handlers.py @@ -0,0 +1,582 @@ +"""Tests for JSON serialization handlers for PolyBus Python implementation.""" + +import pytest +import json +from typing import Any, Dict, List, Optional, Callable, Awaitable + +from src.transport.transaction.message.handlers.serializers.json_handlers import JsonHandlers, InvalidOperationError +from src.transport.transaction.incoming_transaction import IncomingTransaction +from src.transport.transaction.outgoing_transaction import OutgoingTransaction +from src.transport.transaction.message.incoming_message import IncomingMessage +from src.transport.transaction.message.messages import Messages +from src.transport.transaction.message.message_info import message_info +from src.transport.transaction.message.message_type import MessageType +from src.headers import Headers + + +class MockTransport: + """Mock implementation of ITransport.""" + + @property + def supports_command_messages(self) -> bool: + return True + + @property + def supports_delayed_messages(self) -> bool: + return True + + @property + def supports_subscriptions(self) -> bool: + return False + + async def send(self, transaction) -> None: + pass + + async def subscribe(self, message_info) -> None: + pass + + async def start(self) -> None: + pass + + async def stop(self) -> None: + pass + + +class MockPolyBus: + """Mock implementation of IPolyBus.""" + + def __init__(self, messages: Messages): + self._messages = messages + self._transport = MockTransport() + self._properties = {} + self._incoming_handlers = [] + self._outgoing_handlers = [] + self._name = "MockBus" + + @property + def properties(self) -> Dict[str, Any]: + return self._properties + + @property + def transport(self) -> MockTransport: + return self._transport + + @property + def incoming_handlers(self) -> List: + return self._incoming_handlers + + @property + def outgoing_handlers(self) -> List: + return self._outgoing_handlers + + @property + def messages(self) -> Messages: + return self._messages + + @property + def name(self) -> str: + return self._name + + async def create_transaction(self, message=None): + if message is None: + return MockOutgoingTransaction(self) + return MockIncomingTransaction(self, message) + + async def send(self, transaction) -> None: + pass + + async def start(self) -> None: + pass + + async def stop(self) -> None: + pass + + +class MockOutgoingTransaction(OutgoingTransaction): + """Mock outgoing transaction.""" + + def __init__(self, bus): + super().__init__(bus) + + async def abort(self) -> None: + pass + + async def commit(self) -> None: + pass + + +class MockIncomingTransaction(IncomingTransaction): + """Mock incoming transaction.""" + + def __init__(self, bus, incoming_message): + super().__init__(bus, incoming_message) + + async def abort(self) -> None: + pass + + async def commit(self) -> None: + pass + + +@message_info(MessageType.COMMAND, "test-service", "TestMessage", 1, 0, 0) +class SampleMessage: + """Sample message class.""" + + def __init__(self, id: int = 0, name: str = ""): + self.id = id + self.name = name + + def to_dict(self): + """Convert to dictionary for JSON serialization.""" + return {"id": self.id, "name": self.name} + + +class UnknownMessage: + """Message class without message_info decorator.""" + + def __init__(self, data: str = ""): + self.data = data + + def to_dict(self): + """Convert to dictionary for JSON serialization.""" + return {"data": self.data} + + +class TestJsonHandlers: + """Test cases for JsonHandlers class.""" + + def setup_method(self): + """Set up test fixtures before each test method.""" + self.json_handlers = JsonHandlers() + self.messages = Messages() + self.mock_bus = MockPolyBus(self.messages) + + # Add the test message type to the messages collection + self.messages.add(SampleMessage) + self.header = "endpoint=test-service, type=command, name=TestMessage, version=1.0.0" + + # Deserializer Tests + + @pytest.mark.asyncio + async def test_deserializer_with_valid_type_header_deserializes_message(self): + """Test that valid type header deserializes message correctly.""" + # Arrange + test_message = SampleMessage(1, "Test") + serialized_body = json.dumps({"id": test_message.id, "name": test_message.name}) + + incoming_message = IncomingMessage(self.mock_bus, serialized_body) + incoming_message.headers[Headers.MESSAGE_TYPE] = self.header + + transaction = MockIncomingTransaction(self.mock_bus, incoming_message) + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await self.json_handlers.deserializer(transaction, next_handler) + + # Assert + assert next_called is True + assert incoming_message.message is not None + assert isinstance(incoming_message.message, dict) + assert incoming_message.message["id"] == 1 + assert incoming_message.message["name"] == "Test" + assert incoming_message.message_type == SampleMessage + + @pytest.mark.asyncio + async def test_deserializer_with_custom_json_options_deserializes_with_options(self): + """Test that custom JSON options are used during deserialization.""" + # Arrange + json_options = {"parse_float": lambda x: int(float(x))} + json_handlers = JsonHandlers(json_options=json_options) + + test_data = {"id": 2.5, "name": "Float"} + serialized_body = json.dumps(test_data) + + incoming_message = IncomingMessage(self.mock_bus, serialized_body) + incoming_message.headers[Headers.MESSAGE_TYPE] = self.header + + transaction = MockIncomingTransaction(self.mock_bus, incoming_message) + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await json_handlers.deserializer(transaction, next_handler) + + # Assert + assert next_called is True + assert incoming_message.message["id"] == 2 # Should be converted to int + assert incoming_message.message["name"] == "Float" + + @pytest.mark.asyncio + async def test_deserializer_with_unknown_type_and_throw_on_missing_type_false_parses_as_json(self): + """Test that unknown type with throw_on_missing_type=False parses as generic JSON.""" + # Arrange + json_handlers = JsonHandlers(throw_on_missing_type=False) + test_object = {"id": 3, "name": "Unknown"} + serialized_body = json.dumps(test_object) + header = "endpoint=test-service, type=command, name=UnknownMessage, version=1.0.0" + + incoming_message = IncomingMessage(self.mock_bus, serialized_body) + incoming_message.headers[Headers.MESSAGE_TYPE] = header + + transaction = MockIncomingTransaction(self.mock_bus, incoming_message) + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await json_handlers.deserializer(transaction, next_handler) + + # Assert + assert next_called is True + assert incoming_message.message is not None + assert isinstance(incoming_message.message, dict) + assert incoming_message.message["id"] == 3 + assert incoming_message.message["name"] == "Unknown" + + @pytest.mark.asyncio + async def test_deserializer_with_unknown_type_and_throw_on_missing_type_true_throws_exception(self): + """Test that unknown type with throw_on_missing_type=True throws exception.""" + # Arrange + json_handlers = JsonHandlers(throw_on_missing_type=True) + test_object = {"id": 4, "name": "Error"} + serialized_body = json.dumps(test_object) + header = "endpoint=test-service, type=command, name=UnknownMessage, version=1.0.0" + + incoming_message = IncomingMessage(self.mock_bus, serialized_body) + incoming_message.headers[Headers.MESSAGE_TYPE] = header + + transaction = MockIncomingTransaction(self.mock_bus, incoming_message) + + async def next_handler(): + pass + + # Act & Assert + with pytest.raises(InvalidOperationError) as exc_info: + await json_handlers.deserializer(transaction, next_handler) + + assert "The type header is missing, invalid, or if the type cannot be found." in str(exc_info.value) + + @pytest.mark.asyncio + async def test_deserializer_with_missing_type_header_throws_exception(self): + """Test that missing type header throws exception when throw_on_missing_type=True.""" + # Arrange + json_handlers = JsonHandlers(throw_on_missing_type=True) + incoming_message = IncomingMessage(self.mock_bus, "{}") + # No MESSAGE_TYPE header set + + transaction = MockIncomingTransaction(self.mock_bus, incoming_message) + + async def next_handler(): + pass + + # Act & Assert + with pytest.raises(InvalidOperationError) as exc_info: + await json_handlers.deserializer(transaction, next_handler) + + assert "The type header is missing, invalid, or if the type cannot be found." in str(exc_info.value) + + @pytest.mark.asyncio + async def test_deserializer_with_invalid_json_throws_json_exception(self): + """Test that invalid JSON throws JSONDecodeError.""" + # Arrange + incoming_message = IncomingMessage(self.mock_bus, "invalid json") + incoming_message.headers[Headers.MESSAGE_TYPE] = self.header + + transaction = MockIncomingTransaction(self.mock_bus, incoming_message) + + async def next_handler(): + pass + + # Act & Assert + with pytest.raises(json.JSONDecodeError): + await self.json_handlers.deserializer(transaction, next_handler) + + # Serializer Tests + + @pytest.mark.asyncio + async def test_serializer_with_valid_message_serializes_and_sets_headers(self): + """Test that valid message serializes correctly and sets headers.""" + # Arrange + test_message = {"id": 5, "name": "Serialize"} + + mock_transaction = MockOutgoingTransaction(self.mock_bus) + outgoing_message = mock_transaction.add_outgoing_message(test_message, "test-endpoint") + # Manually set the message type for the test + outgoing_message.message_type = SampleMessage + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await self.json_handlers.serializer(mock_transaction, next_handler) + + # Assert + assert next_called is True + assert outgoing_message.body is not None + + # Deserialize to verify content + deserialized_message = json.loads(outgoing_message.body) + assert deserialized_message["id"] == 5 + assert deserialized_message["name"] == "Serialize" + + assert outgoing_message.headers[Headers.CONTENT_TYPE] == "application/json" + assert outgoing_message.headers[Headers.MESSAGE_TYPE] == self.header + + @pytest.mark.asyncio + async def test_serializer_with_custom_content_type_uses_custom_content_type(self): + """Test that custom content type is used.""" + # Arrange + custom_content_type = "application/custom-json" + json_handlers = JsonHandlers(content_type=custom_content_type, throw_on_invalid_type=False) + + test_message = {"id": 6, "name": "Custom"} + + mock_transaction = MockOutgoingTransaction(self.mock_bus) + outgoing_message = mock_transaction.add_outgoing_message(test_message, "test-endpoint") + # Manually set the message type for the test + outgoing_message.message_type = SampleMessage + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await json_handlers.serializer(mock_transaction, next_handler) + + # Assert + assert next_called is True + assert outgoing_message.headers[Headers.CONTENT_TYPE] == custom_content_type + + @pytest.mark.asyncio + async def test_serializer_with_custom_json_options_serializes_with_options(self): + """Test that custom JSON options are used during serialization.""" + # Arrange + json_options = {"indent": 2, "sort_keys": True} + json_handlers = JsonHandlers(json_options=json_options, throw_on_invalid_type=False) + + test_message = {"id": 7, "name": "Options"} + + mock_transaction = MockOutgoingTransaction(self.mock_bus) + outgoing_message = mock_transaction.add_outgoing_message(test_message, "test-endpoint") + # Manually set the message type for the test + outgoing_message.message_type = SampleMessage + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await json_handlers.serializer(mock_transaction, next_handler) + + # Assert + assert next_called is True + assert outgoing_message.body is not None + # Should have indentation (newlines) due to indent=2 + assert "\n" in outgoing_message.body + # Should be sorted due to sort_keys=True + lines = outgoing_message.body.split('\n') + # Find lines with keys and verify order + key_lines = [line.strip() for line in lines if ':' in line and '"' in line] + if len(key_lines) >= 2: + # Should be alphabetical order: "id" before "name" + assert "id" in key_lines[0] + assert "name" in key_lines[1] + + @pytest.mark.asyncio + async def test_serializer_with_unknown_type_and_throw_on_invalid_type_false_skips_header_setting(self): + """Test that unknown type with throw_on_invalid_type=False skips header setting.""" + # Arrange + json_handlers = JsonHandlers(throw_on_invalid_type=False) + + test_message = {"data": "test"} + + mock_transaction = MockOutgoingTransaction(self.mock_bus) + outgoing_message = mock_transaction.add_outgoing_message(test_message, "unknown-endpoint") + # Set the message type to UnknownMessage (not registered) + outgoing_message.message_type = UnknownMessage + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await json_handlers.serializer(mock_transaction, next_handler) + + # Assert + assert next_called is True + assert outgoing_message.body is not None + assert outgoing_message.headers[Headers.CONTENT_TYPE] == "application/json" + assert Headers.MESSAGE_TYPE not in outgoing_message.headers + + @pytest.mark.asyncio + async def test_serializer_with_unknown_type_and_throw_on_invalid_type_true_throws_exception(self): + """Test that unknown type with throw_on_invalid_type=True throws exception.""" + # Arrange + json_handlers = JsonHandlers(throw_on_invalid_type=True) + + test_message = {"data": "error"} + + mock_transaction = MockOutgoingTransaction(self.mock_bus) + outgoing_message = mock_transaction.add_outgoing_message(test_message, "unknown-endpoint") + # Set the message type to UnknownMessage (not registered) + outgoing_message.message_type = UnknownMessage + + async def next_handler(): + pass + + # Act & Assert + with pytest.raises(InvalidOperationError) as exc_info: + await json_handlers.serializer(mock_transaction, next_handler) + + assert "The header has an invalid type." in str(exc_info.value) + + @pytest.mark.asyncio + async def test_serializer_with_multiple_messages_serializes_all(self): + """Test that multiple messages are all serialized.""" + # Arrange + test_message1 = {"id": 8, "name": "First"} + test_message2 = {"id": 9, "name": "Second"} + + mock_transaction = MockOutgoingTransaction(self.mock_bus) + outgoing_message1 = mock_transaction.add_outgoing_message(test_message1, "test-endpoint") + outgoing_message2 = mock_transaction.add_outgoing_message(test_message2, "test-endpoint") + # Manually set the message types for the test + outgoing_message1.message_type = SampleMessage + outgoing_message2.message_type = SampleMessage + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await self.json_handlers.serializer(mock_transaction, next_handler) + + # Assert + assert next_called is True + assert outgoing_message1.body is not None + assert outgoing_message2.body is not None + + deserialized_message1 = json.loads(outgoing_message1.body) + deserialized_message2 = json.loads(outgoing_message2.body) + + assert deserialized_message1["id"] == 8 + assert deserialized_message1["name"] == "First" + assert deserialized_message2["id"] == 9 + assert deserialized_message2["name"] == "Second" + + @pytest.mark.asyncio + async def test_serializer_with_empty_outgoing_messages_calls_next(self): + """Test that empty outgoing messages still calls next handler.""" + # Arrange + mock_transaction = MockOutgoingTransaction(self.mock_bus) + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await self.json_handlers.serializer(mock_transaction, next_handler) + + # Assert + assert next_called is True + + def test_invalid_operation_error_is_exception(self): + """Test that InvalidOperationError is an exception.""" + # Act + error = InvalidOperationError("Test error") + + # Assert + assert isinstance(error, Exception) + assert str(error) == "Test error" + + def test_json_handlers_default_initialization(self): + """Test JsonHandlers default initialization.""" + # Act + handlers = JsonHandlers() + + # Assert + assert handlers.json_options == {} + assert handlers.content_type == "application/json" + assert handlers.throw_on_missing_type is True + assert handlers.throw_on_invalid_type is True + + def test_json_handlers_custom_initialization(self): + """Test JsonHandlers custom initialization.""" + # Arrange + json_options = {"indent": 4} + content_type = "application/vnd.api+json" + throw_on_missing_type = False + throw_on_invalid_type = False + + # Act + handlers = JsonHandlers( + json_options=json_options, + content_type=content_type, + throw_on_missing_type=throw_on_missing_type, + throw_on_invalid_type=throw_on_invalid_type + ) + + # Assert + assert handlers.json_options == json_options + assert handlers.content_type == content_type + assert handlers.throw_on_missing_type is False + assert handlers.throw_on_invalid_type is False + + @pytest.mark.asyncio + async def test_deserializer_with_missing_type_header_and_throw_false_parses_generic_json(self): + """Test that missing type header with throw_on_missing_type=False parses as generic JSON.""" + # Arrange + json_handlers = JsonHandlers(throw_on_missing_type=False) + test_object = {"id": 10, "name": "NoHeader"} + serialized_body = json.dumps(test_object) + + incoming_message = IncomingMessage(self.mock_bus, serialized_body) + # No MESSAGE_TYPE header set + + transaction = MockIncomingTransaction(self.mock_bus, incoming_message) + + next_called = False + + async def next_handler(): + nonlocal next_called + next_called = True + + # Act + await json_handlers.deserializer(transaction, next_handler) + + # Assert + assert next_called is True + assert incoming_message.message is not None + assert isinstance(incoming_message.message, dict) + assert incoming_message.message["id"] == 10 + assert incoming_message.message["name"] == "NoHeader" + assert incoming_message.message_type == str # Should remain as default string type + + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file diff --git a/src/python/tests/transport/transaction/message/test_message_info.py b/src/python/tests/transport/transaction/message/test_message_info.py new file mode 100644 index 0000000..add7133 --- /dev/null +++ b/src/python/tests/transport/transaction/message/test_message_info.py @@ -0,0 +1,277 @@ +"""Tests for the MessageInfo decorator.""" + +import pytest +from src.transport.transaction.message.message_info import message_info, MessageInfo +from src.transport.transaction.message.message_type import MessageType + + +def test_get_attribute_from_header_with_valid_header_returns_correct_attribute(): + """Test parsing valid header strings.""" + # Arrange + header = "endpoint=user-service, type=Command, name=CreateUser, version=1.2.3" + + # Act + result = MessageInfo.get_attribute_from_header(header) + + # Assert + assert result is not None + assert result.endpoint == "user-service" + assert result.message_type == MessageType.COMMAND + assert result.name == "CreateUser" + assert result.major == 1 + assert result.minor == 2 + assert result.patch == 3 + + +def test_get_attribute_from_header_with_event_type_returns_correct_attribute(): + """Test parsing header with event type.""" + # Arrange + header = "endpoint=notification-service, type=Event, name=UserCreated, version=2.0.1" + + # Act + result = MessageInfo.get_attribute_from_header(header) + + # Assert + assert result is not None + assert result.endpoint == "notification-service" + assert result.message_type == MessageType.EVENT + assert result.name == "UserCreated" + assert result.major == 2 + assert result.minor == 0 + assert result.patch == 1 + + +def test_get_attribute_from_header_with_extra_spaces_returns_correct_attribute(): + """Test parsing header with extra spacing.""" + # Arrange - the current regex doesn't handle spaces within values well, so testing valid spacing + header = "endpoint=payment-service, type=Command, name=ProcessPayment, version=3.14.159" + + # Act + result = MessageInfo.get_attribute_from_header(header) + + # Assert + assert result is not None + assert result.endpoint == "payment-service" + assert result.message_type == MessageType.COMMAND + assert result.name == "ProcessPayment" + assert result.major == 3 + assert result.minor == 14 + assert result.patch == 159 + + +def test_get_attribute_from_header_with_case_insensitive_type_returns_correct_attribute(): + """Test parsing header with case insensitive type.""" + # Arrange + header = "endpoint=order-service, type=command, name=PlaceOrder, version=1.0.0" + + # Act + result = MessageInfo.get_attribute_from_header(header) + + # Assert + assert result is not None + assert result.message_type == MessageType.COMMAND + + +@pytest.mark.parametrize("header", [ + "", + "invalid header", + "endpoint=test", + "endpoint=test, type=Command", + "endpoint=test, type=Command, name=Test", + "endpoint=test, type=Command, name=Test, version=invalid", + "type=Command, name=Test, version=1.0.0", +]) +def test_get_attribute_from_header_with_invalid_header_returns_none(header): + """Test parsing invalid header strings returns None.""" + # Act + result = MessageInfo.get_attribute_from_header(header) + + # Assert + assert result is None + + +def test_get_attribute_from_header_with_invalid_enum_type_returns_none(): + """Test parsing header with invalid enum type returns None (Python doesn't throw like C#).""" + # Arrange + header = "endpoint=test, type=InvalidType, name=Test, version=1.0.0" + + # Act + result = MessageInfo.get_attribute_from_header(header) + + # Assert + assert result is None + + +def test_get_attribute_from_header_with_missing_version_returns_none(): + """Test parsing header with missing version.""" + # Arrange + header = "endpoint=test-service, type=Command, name=TestCommand, version=" + + # Act + result = MessageInfo.get_attribute_from_header(header) + + # Assert + assert result is None + + +def test_get_attribute_from_header_with_incomplete_version_returns_none(): + """Test parsing header with incomplete version.""" + # Arrange + header = "endpoint=test-service, type=Command, name=TestCommand, version=1.0" + + # Act + result = MessageInfo.get_attribute_from_header(header) + + # Assert + assert result is None + + +def test_equals_with_identical_attributes_returns_true(): + """Test equality with identical attributes.""" + # Arrange + attr1 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + attr2 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + + # Act & Assert + assert attr1 == attr2 + assert attr2 == attr1 + + +def test_equals_with_same_object_returns_true(): + """Test equality with same object.""" + # Arrange + attr = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + + # Act & Assert + assert attr == attr + + +def test_equals_with_different_type_returns_false(): + """Test equality with different message type.""" + # Arrange + attr1 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + attr2 = MessageInfo(MessageType.EVENT, "user-service", "CreateUser", 1, 2, 3) + + # Act & Assert + assert attr1 != attr2 + + +def test_equals_with_different_endpoint_returns_false(): + """Test equality with different endpoint.""" + # Arrange + attr1 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + attr2 = MessageInfo(MessageType.COMMAND, "order-service", "CreateUser", 1, 2, 3) + + # Act & Assert + assert attr1 != attr2 + + +def test_equals_with_different_name_returns_false(): + """Test equality with different name.""" + # Arrange + attr1 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + attr2 = MessageInfo(MessageType.COMMAND, "user-service", "UpdateUser", 1, 2, 3) + + # Act & Assert + assert attr1 != attr2 + + +def test_equals_with_different_major_version_returns_false(): + """Test equality with different major version.""" + # Arrange + attr1 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + attr2 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 2, 2, 3) + + # Act & Assert + assert attr1 != attr2 + + +def test_equals_with_different_minor_version_returns_true(): + """Test equality with different minor version (should be equal).""" + # Arrange + attr1 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + attr2 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 3, 3) + + # Act & Assert + assert attr1 == attr2, "Minor version differences should not affect equality" + + +def test_equals_with_different_patch_version_returns_true(): + """Test equality with different patch version (should be equal).""" + # Arrange + attr1 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + attr2 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 4) + + # Act & Assert + assert attr1 == attr2, "Patch version differences should not affect equality" + + +def test_equals_with_null_object_returns_false(): + """Test equality with None object.""" + # Arrange + attr = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + + # Act & Assert + assert attr != None + + +def test_equals_with_different_object_type_returns_false(): + """Test equality with different object type.""" + # Arrange + attr = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + other = "not a MessageAttribute" + + # Act & Assert + assert attr != other + + +def test_get_hash_code_with_identical_attributes_returns_same_hash_code(): + """Test that identical attributes have same hash code.""" + # Arrange + attr1 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + attr2 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + + # Act & Assert + assert hash(attr1) == hash(attr2) + + +def test_get_hash_code_with_different_attributes_returns_different_hash_codes(): + """Test that different attributes have different hash codes.""" + # Arrange + attr1 = MessageInfo(MessageType.COMMAND, "user-service", "CreateUser", 1, 2, 3) + attr2 = MessageInfo(MessageType.EVENT, "user-service", "CreateUser", 1, 2, 3) + + # Act & Assert + assert hash(attr1) != hash(attr2) + + +# Additional Python-specific tests for decorator functionality +def test_message_info_decorator_basic_usage(): + """Test that the decorator can be applied to a class.""" + + @message_info(MessageType.COMMAND, "test-endpoint", "test-name", 1, 0, 0) + class TestClass: + pass + + assert hasattr(TestClass, '_message_info') + assert isinstance(TestClass._message_info, MessageInfo) + assert TestClass._message_info.message_type == MessageType.COMMAND + assert TestClass._message_info.endpoint == "test-endpoint" + assert TestClass._message_info.name == "test-name" + assert TestClass._message_info.major == 1 + assert TestClass._message_info.minor == 0 + assert TestClass._message_info.patch == 0 + + +def test_message_info_to_string(): + """Test the to_string method.""" + info = MessageInfo(MessageType.EVENT, "user-service", "user-created", 2, 1, 3) + + # With version + expected_with_version = "endpoint=user-service, type=event, name=user-created, version=2.1.3" + assert info.to_string(True) == expected_with_version + assert str(info) == expected_with_version + + # Without version + expected_without_version = "endpoint=user-service, type=event, name=user-created" + assert info.to_string(False) == expected_without_version \ No newline at end of file diff --git a/src/python/tests/transport/transaction/message/test_messages.py b/src/python/tests/transport/transaction/message/test_messages.py new file mode 100644 index 0000000..fcde972 --- /dev/null +++ b/src/python/tests/transport/transaction/message/test_messages.py @@ -0,0 +1,237 @@ +""" +Tests for the Messages class. +""" +import pytest +from src.transport.transaction.message.messages import Messages +from src.transport.transaction.message.message_info import MessageInfo, message_info +from src.transport.transaction.message.message_info import MessageType + + +class TestMessages: + """Test cases for the Messages class.""" + + def setup_method(self): + """Set up test fixtures before each test method.""" + self.messages = Messages() + + # Test Message Classes + @message_info(MessageType.COMMAND, "OrderService", "CreateOrder", 1, 0, 0) + class CreateOrderCommand: + def __init__(self): + self.order_id: str = "" + self.amount: float = 0.0 + + @message_info(MessageType.EVENT, "OrderService", "OrderCreated", 2, 1, 3) + class OrderCreatedEvent: + def __init__(self): + self.order_id: str = "" + self.created_at: str = "" + + @message_info(MessageType.COMMAND, "PaymentService", "ProcessPayment", 1, 5, 2) + class ProcessPaymentCommand: + def __init__(self): + self.payment_id: str = "" + self.amount: float = 0.0 + + class MessageWithoutAttribute: + def __init__(self): + self.data: str = "" + + def test_add_valid_message_type_returns_message_info(self): + """Test adding a valid message type returns MessageInfo.""" + # Act + result = self.messages.add(TestMessages.CreateOrderCommand) + + # Assert + assert result is not None + assert result.message_type == MessageType.COMMAND + assert result.endpoint == "OrderService" + assert result.name == "CreateOrder" + assert result.major == 1 + assert result.minor == 0 + assert result.patch == 0 + + def test_add_message_type_without_attribute_raises_value_error(self): + """Test adding a message type without attribute raises ValueError.""" + # Act & Assert + with pytest.raises(ValueError) as excinfo: + self.messages.add(TestMessages.MessageWithoutAttribute) + + assert "does not have a MessageInfo decorator" in str(excinfo.value) + assert TestMessages.MessageWithoutAttribute.__name__ in str(excinfo.value) + + def test_get_message_info_existing_type_returns_correct_message_info(self): + """Test getting message info for existing type returns correct info.""" + # Arrange + self.messages.add(TestMessages.CreateOrderCommand) + + # Act + result = self.messages.get_message_info(TestMessages.CreateOrderCommand) + + # Assert + assert result is not None + assert result.message_type == MessageType.COMMAND + assert result.endpoint == "OrderService" + assert result.name == "CreateOrder" + + def test_get_message_info_non_existent_type_returns_none(self): + """Test getting message info for non-existent type returns None.""" + # Act + result = self.messages.get_message_info(TestMessages.CreateOrderCommand) + + # Assert + assert result is None + + def test_get_header_existing_type_returns_correct_header(self): + """Test getting header for existing type returns correct header.""" + # Arrange + self.messages.add(TestMessages.OrderCreatedEvent) + + # Act + result = self.messages.get_header(TestMessages.OrderCreatedEvent) + + # Assert + assert result == "endpoint=OrderService, type=event, name=OrderCreated, version=2.1.3" + + def test_get_header_non_existent_type_returns_none(self): + """Test getting header for non-existent type returns None.""" + # Act + result = self.messages.get_header(TestMessages.CreateOrderCommand) + + # Assert + assert result is None + + def test_get_type_by_header_valid_header_returns_correct_type(self): + """Test getting type by valid header returns correct type.""" + # Arrange + self.messages.add(TestMessages.ProcessPaymentCommand) + header = "endpoint=PaymentService, type=command, name=ProcessPayment, version=1.5.2" + + # Act + result = self.messages.get_type_by_header(header) + + # Assert + assert result == TestMessages.ProcessPaymentCommand + + def test_get_type_by_header_invalid_header_returns_none(self): + """Test getting type by invalid header returns None.""" + # Arrange + invalid_header = "invalid header format" + + # Act + result = self.messages.get_type_by_header(invalid_header) + + # Assert + assert result is None + + def test_get_type_by_header_non_existent_message_returns_none(self): + """Test getting type by header for non-existent message returns None.""" + # Arrange + header = "endpoint=UnknownService, type=command, name=UnknownCommand, version=1.0.0" + + # Act + result = self.messages.get_type_by_header(header) + + # Assert + assert result is None + + def test_get_type_by_header_caches_results(self): + """Test that get_type_by_header caches results.""" + # Arrange + self.messages.add(TestMessages.CreateOrderCommand) + header = "endpoint=OrderService, type=command, name=CreateOrder, version=1.0.0" + + # Act + result1 = self.messages.get_type_by_header(header) + result2 = self.messages.get_type_by_header(header) + + # Assert + assert result1 == TestMessages.CreateOrderCommand + assert result2 == TestMessages.CreateOrderCommand + assert result1 is result2 + + def test_get_type_by_message_info_existing_message_info_returns_correct_type(self): + """Test getting type by existing MessageInfo returns correct type.""" + # Arrange + self.messages.add(TestMessages.OrderCreatedEvent) + message_info = MessageInfo(MessageType.EVENT, "OrderService", "OrderCreated", 2, 1, 3) + + # Act + result = self.messages.get_type_by_message_info(message_info) + + # Assert + assert result == TestMessages.OrderCreatedEvent + + def test_get_type_by_message_info_non_existent_message_info_returns_none(self): + """Test getting type by non-existent MessageInfo returns None.""" + # Arrange + message_info = MessageInfo(MessageType.COMMAND, "UnknownService", "UnknownCommand", 1, 0, 0) + + # Act + result = self.messages.get_type_by_message_info(message_info) + + # Assert + assert result is None + + def test_get_type_by_message_info_different_minor_patch_versions_returns_type(self): + """Test getting type by MessageInfo with different minor/patch versions returns type.""" + # Arrange + self.messages.add(TestMessages.OrderCreatedEvent) # Has version 2.1.3 + message_info_different_minor = MessageInfo(MessageType.EVENT, "OrderService", "OrderCreated", 2, 5, 3) + message_info_different_patch = MessageInfo(MessageType.EVENT, "OrderService", "OrderCreated", 2, 1, 9) + + # Act + result1 = self.messages.get_type_by_message_info(message_info_different_minor) + result2 = self.messages.get_type_by_message_info(message_info_different_patch) + + # Assert + assert result1 == TestMessages.OrderCreatedEvent + assert result2 == TestMessages.OrderCreatedEvent + + def test_get_type_by_message_info_different_major_version_returns_none(self): + """Test getting type by MessageInfo with different major version returns None.""" + # Arrange + self.messages.add(TestMessages.OrderCreatedEvent) # Has version 2.1.3 + message_info_different_major = MessageInfo(MessageType.EVENT, "OrderService", "OrderCreated", 3, 1, 3) + + # Act + result = self.messages.get_type_by_message_info(message_info_different_major) + + # Assert + assert result is None + + def test_multiple_messages_all_methods_work_correctly(self): + """Test that all methods work correctly with multiple messages.""" + # Arrange + self.messages.add(TestMessages.CreateOrderCommand) + self.messages.add(TestMessages.OrderCreatedEvent) + self.messages.add(TestMessages.ProcessPaymentCommand) + + # Act & Assert - get_message_info + command_info = self.messages.get_message_info(TestMessages.CreateOrderCommand) + event_info = self.messages.get_message_info(TestMessages.OrderCreatedEvent) + payment_info = self.messages.get_message_info(TestMessages.ProcessPaymentCommand) + + assert command_info.message_type == MessageType.COMMAND + assert event_info.message_type == MessageType.EVENT + assert payment_info.endpoint == "PaymentService" + + # Act & Assert - get_header + command_header = self.messages.get_header(TestMessages.CreateOrderCommand) + event_header = self.messages.get_header(TestMessages.OrderCreatedEvent) + + assert "OrderService" in command_header + assert "OrderCreated" in event_header + + # Act & Assert - get_type_by_header + type_from_header = self.messages.get_type_by_header(command_header) + assert type_from_header == TestMessages.CreateOrderCommand + + def test_add_same_type_twice_raises_key_error(self): + """Test adding the same type twice raises KeyError.""" + # Arrange + self.messages.add(TestMessages.CreateOrderCommand) + + # Act & Assert + with pytest.raises(KeyError): + self.messages.add(TestMessages.CreateOrderCommand) \ No newline at end of file diff --git a/src/typescript/.editorconfig b/src/typescript/.editorconfig new file mode 100644 index 0000000..b2e4137 --- /dev/null +++ b/src/typescript/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{ts,js,json}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/src/typescript/.npmrc b/src/typescript/.npmrc new file mode 100644 index 0000000..227c05a --- /dev/null +++ b/src/typescript/.npmrc @@ -0,0 +1,8 @@ +# This file is used during publishing from CI/CD +# The NPM_TOKEN environment variable should be set in GitHub Secrets +# For publishing to npm registry: +# //registry.npmjs.org/:_authToken=${NPM_TOKEN} + +# For publishing to GitHub Packages (uncomment if using): +# @CyAScott:registry=https://npm.pkg.github.com +# //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} diff --git a/src/typescript/.vscode/settings.json b/src/typescript/.vscode/settings.json new file mode 100644 index 0000000..7e837c3 --- /dev/null +++ b/src/typescript/.vscode/settings.json @@ -0,0 +1,45 @@ +{ + "settings": { + // Jest Test Explorer settings + "jest.autoRun": "off", + "jest.showCoverageOnLoad": true, + "jest.coverageFormatter": "DefaultFormatter", + "jest.coverageColors": { + "covered": "rgba(9, 156, 65, 0.4)", + "uncovered": "rgba(121, 31, 10, 0.4)", + "partially": "rgba(235, 198, 52, 0.4)" + }, + + // TypeScript settings for tests + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.suggest.autoImports": true, + + // File associations + "files.associations": { + "*.test.ts": "typescript", + "*.spec.ts": "typescript" + }, + + // Exclude coverage directory from file explorer + "files.exclude": { + "**/coverage": true, + "**/node_modules": true, + "**/dist": true + }, + + // Test file templates + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": true + } + }, + + "extensions": { + "recommendations": [ + "orta.vscode-jest", + "ms-vscode.vscode-typescript-next", + "bradlc.vscode-tailwindcss" + ] + } +} \ No newline at end of file diff --git a/src/typescript/.vscode/tasks.json b/src/typescript/.vscode/tasks.json new file mode 100644 index 0000000..253c99a --- /dev/null +++ b/src/typescript/.vscode/tasks.json @@ -0,0 +1,65 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "command": "npm", + "args": ["run", "build"], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": ["$tsc"], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + } + }, + { + "label": "dev", + "type": "shell", + "command": "npm", + "args": ["run", "dev"], + "group": "build", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "source": "ts", + "applyTo": "closedDocuments", + "fileLocation": ["relative", "${workspaceRoot}"], + "pattern": "$tsc-watch", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(\\s|^)Starting compilation in watch mode\\.\\.\\.(\\s|$)" + }, + "endsPattern": { + "regexp": "(\\s|^)Found \\d+ errors?\\. Watching for file changes\\.(\\s|$)" + } + } + }, + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + } + }, + { + "label": "clean", + "type": "shell", + "command": "npm", + "args": ["run", "clean"], + "group": "build", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared" + } + } + ] +} \ No newline at end of file diff --git a/src/typescript/README.md b/src/typescript/README.md new file mode 100644 index 0000000..21ff7bd --- /dev/null +++ b/src/typescript/README.md @@ -0,0 +1,306 @@ +# PolyBus TypeScript + +A TypeScript implementation of the PolyBus messaging library, providing a unified interface for message transport across different messaging systems. This package is designed to work seamlessly in both Node.js and browser environments. + +## Prerequisites + +- [Node.js 14.0+](https://nodejs.org/) (tested with Node.js 14-20) +- npm or yarn package manager +- Any IDE that supports TypeScript development (VS Code, WebStorm, etc.) + +## Project Structure + +``` +src/typescript/ +├── src/ # Source code +│ ├── index.ts # Main entry point +│ ├── i-poly-bus.ts # Main interface +│ ├── poly-bus.ts # Core implementation +│ ├── poly-bus-builder.ts # Builder pattern implementation +│ ├── headers.ts # Message headers +│ ├── transport/ # Transport implementations +│ │ ├── i-transport.ts # Transport interface +│ │ └── ... +│ └── __tests__/ # Test files +│ ├── poly-bus.test.ts # Test implementations +│ └── headers.test.ts +├── dist/ # Compiled output +│ ├── index.js # CommonJS build +│ ├── index.mjs # ES Module build +│ ├── index.umd.js # UMD build (browser) +│ └── index.d.ts # TypeScript declarations +├── package.json # Project configuration and dependencies +├── tsconfig.json # TypeScript configuration +├── jest.config.js # Jest testing configuration +├── eslint.config.js # ESLint configuration +├── rollup.config.js # Rollup bundler configuration +└── README.md # This file +``` + +## Quick Start + +### Installing Dependencies + +```bash +# Navigate to the typescript directory +cd src/typescript + +# Install dependencies +npm install +# Or with yarn: +yarn install +``` + +### Building the Project + +```bash +# Build all formats (CommonJS, ESM, UMD, types) +npm run build + +# Build and watch for changes +npm run dev + +# Clean build artifacts +npm run clean +``` + +### Running Tests + +```bash +# Run all tests +npm test +# Or: npm run test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage + +# Run tests for CI/CD +npm run test:ci +``` + +## Development Workflow + +### Code Quality and Linting + +This project includes comprehensive code analysis and formatting tools: + +```bash +# Run ESLint to check code quality +npm run lint + +# Auto-fix linting issues +npm run lint:fix + +# TypeScript type checking is performed during build +npm run build +``` + +### IDE Integration + +#### Visual Studio Code +1. Install the ESLint extension +2. Install the TypeScript extension (usually built-in) +3. Open the `src/typescript` folder in VS Code +4. Auto-formatting and linting will work automatically + +#### WebStorm / IntelliJ IDEA +1. Open the `src/typescript` folder as a project +2. Enable ESLint integration (Settings → Languages & Frameworks → JavaScript → Code Quality Tools → ESLint) +3. TypeScript support is built-in + +## Configuration + +### Package Configuration + +The project uses modern TypeScript packaging with dual module support: + +- **Target**: ES2018 +- **Module Formats**: CommonJS, ESM, UMD (browser) +- **Node Version**: 14.0+ +- **Type Safety**: Strict mode enabled +- **Decorators**: Experimental decorators enabled + +### Build Outputs + +The package exports multiple formats for maximum compatibility: + +```javascript +{ + "main": "./dist/index.js", // CommonJS for Node.js + "module": "./dist/index.mjs", // ES Module for bundlers + "browser": "./dist/index.umd.js", // UMD for browsers + "types": "./dist/index.d.ts" // TypeScript definitions +} +``` + +### Code Style + +Code style is enforced through: +- **ESLint** with TypeScript plugin +- **TypeScript** strict mode +- Consistent formatting rules (2-space indentation, single quotes, semicolons) + +### Testing Configuration + +Jest configuration includes: +- **Test Environment**: Node.js +- **Preset**: ts-jest with ESM support +- **Coverage**: HTML, LCOV, JSON, text reports +- **Coverage Thresholds**: 50% for branches, functions, lines, statements +- **Test Discovery**: `**/*.test.ts`, `**/*.spec.ts` + +## Dependencies + +### Runtime Dependencies +- `reflect-metadata` (^0.2.2) - Metadata reflection for decorators + +### Development Dependencies +- `typescript` (^5.2.2) - TypeScript compiler +- `jest` (^30.2.0) - Testing framework +- `ts-jest` (^29.4.5) - TypeScript support for Jest +- `eslint` (^9.39.0) - Code linting +- `@typescript-eslint/eslint-plugin` (^8.46.2) - TypeScript ESLint rules +- `rollup` (^4.1.4) - Module bundler +- `@rollup/plugin-typescript` (^11.1.5) - Rollup TypeScript plugin +- `@types/node` (^20.8.0) - Node.js type definitions +- `@types/jest` (^30.0.0) - Jest type definitions + +## Common Commands + +```bash +# Development +npm install # Install dependencies +npm run build # Build all formats +npm run dev # Build and watch for changes +npm run clean # Clean build artifacts + +# Testing +npm test # Run tests once +npm run test:watch # Run tests in watch mode +npm run test:coverage # Run tests with coverage report +npm run test:dev # Run tests without thresholds (for development) + +# Code Quality +npm run lint # Check code with ESLint +npm run lint:fix # Auto-fix linting issues + +# Package Management +npm run prepublishOnly # Clean and build (runs before npm publish) +``` + +## Troubleshooting + +### Build Issues + +1. **TypeScript Errors**: Check your TypeScript version + ```bash + npx tsc --version + ``` + +2. **Module Resolution Issues**: Clear node_modules and reinstall + ```bash + rm -rf node_modules package-lock.json + npm install + ``` + +3. **Build Cache Issues**: Clean and rebuild + ```bash + npm run clean + npm run build + ``` + +### Test Issues + +1. **Jest Configuration**: Ensure Jest and ts-jest are properly installed + ```bash + npm install --save-dev jest ts-jest @types/jest + ``` + +2. **ES Module Issues**: Check that `"type": "module"` is in package.json + +3. **Coverage Threshold Errors**: Use `test:dev` during development to bypass thresholds + ```bash + npm run test:dev + ``` + +### Linting Issues + +1. **ESLint Errors**: Auto-fix where possible + ```bash + npm run lint:fix + ``` + +2. **TypeScript Strict Mode**: The project uses strict mode; add proper type annotations + +## Using the Package + +### In Node.js (CommonJS) + +```javascript +const { PolyBus } = require('poly-bus'); + +const bus = new PolyBus(); +// Use the bus... +``` + +### In Node.js (ES Modules) + +```javascript +import { PolyBus } from 'poly-bus'; + +const bus = new PolyBus(); +// Use the bus... +``` + +### In Browser (with bundler) + +```javascript +import { PolyBus } from 'poly-bus'; + +const bus = new PolyBus(); +// Use the bus... +``` + +### In Browser (UMD script tag) + +```html + + +``` + +## Contributing + +1. Follow the established code style (enforced by ESLint) +2. Run `npm run lint:fix` before committing +3. Ensure all tests pass: `npm test` +4. Maintain or improve code coverage +5. Add tests for new functionality +6. Add JSDoc comments for public APIs +7. Update TypeScript types as needed +8. Update documentation as needed + +## Coverage Reports + +After running tests with coverage (`npm run test:coverage`): +- **Terminal**: Coverage summary displayed in terminal +- **HTML**: Detailed report available in `coverage/index.html` +- **LCOV**: Machine-readable report in `coverage/lcov.info` +- **JSON**: Detailed JSON report in `coverage/coverage-final.json` + +## Additional Resources + +- [TypeScript Documentation](https://www.typescriptlang.org/docs/) +- [Jest Documentation](https://jestjs.io/docs/getting-started) +- [ESLint Documentation](https://eslint.org/docs/latest/) +- [Rollup Documentation](https://rollupjs.org/guide/en/) +- [TESTING.md](TESTING.md) - Detailed testing guide + +## License + +See the main project LICENSE file for licensing information. diff --git a/src/typescript/TESTING.md b/src/typescript/TESTING.md new file mode 100644 index 0000000..fc17606 --- /dev/null +++ b/src/typescript/TESTING.md @@ -0,0 +1,192 @@ +# Jest Testing Setup for PolyBus TypeScript + +This document describes the Jest testing setup for the PolyBus TypeScript project. + +## Overview + +Jest has been configured with full TypeScript support, ES modules, and code coverage reporting. The setup includes: + +- **TypeScript Support**: Full compilation and type checking via `ts-jest` +- **ES Modules**: Native ESM support for modern JavaScript +- **Code Coverage**: Comprehensive coverage reporting with multiple output formats +- **Decorator Support**: Support for experimental decorators and metadata + +## Available Test Scripts + +| Script | Description | +|--------|-------------| +| `npm test` | Run all tests with coverage and enforce coverage thresholds | +| `npm run test:dev` | Run tests with coverage but without threshold enforcement (good for development) | +| `npm run test:watch` | Run tests in watch mode for development | +| `npm run test:coverage` | Run tests and generate detailed coverage reports | +| `npm run test:ci` | Run tests in CI mode (no watch, coverage required) | + +## Test File Organization + +Tests should be placed in one of these locations: + +``` +src/ + __tests__/ # Test files here + *.test.ts # Test files ending in .test.ts + *.spec.ts # Test files ending in .spec.ts + component/ + component.test.ts # Tests alongside source files +``` + +## Coverage Configuration + +### Coverage Reports + +Coverage reports are generated in multiple formats: + +- **Console**: Text output during test runs +- **HTML**: Browse `coverage/lcov-report/index.html` for detailed reports +- **LCOV**: `coverage/lcov.info` for CI/CD integration +- **JSON**: `coverage/coverage-final.json` for programmatic access + +### Coverage Thresholds + +Current coverage thresholds (adjust in `jest.config.js`): + +- **Statements**: 50% +- **Branches**: 50% +- **Functions**: 50% +- **Lines**: 50% + +### Coverage Exclusions + +The following files are excluded from coverage: + +- Type definition files (`*.d.ts`) +- Test files (`*.test.ts`, `*.spec.ts`) +- Index files (often just re-exports) +- Template files (`*template*.ts`) + +## Test Examples + +### Basic Test Structure + +```typescript +import { describe, it, expect } from '@jest/globals'; +import { YourComponent } from '../your-component'; + +describe('YourComponent', () => { + it('should create an instance', () => { + const instance = new YourComponent(); + expect(instance).toBeDefined(); + }); +}); +``` + +### With Setup and Teardown + +```typescript +import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; + +describe('YourComponent', () => { + let component: YourComponent; + + beforeEach(() => { + component = new YourComponent(); + }); + + afterEach(() => { + // Cleanup if needed + }); + + it('should work correctly', () => { + expect(component.someMethod()).toBe('expected result'); + }); +}); +``` + +### Mocking + +```typescript +import { jest } from '@jest/globals'; + +// Mock a function +const mockFunction = jest.fn(); +mockFunction.mockReturnValue('mocked value'); + +// Mock a module +jest.mock('../dependency', () => ({ + DependencyClass: jest.fn().mockImplementation(() => ({ + method: jest.fn().mockReturnValue('mocked') + })) +})); +``` + +## Jest Configuration + +The Jest configuration is in `jest.config.js` and includes: + +- **Preset**: `ts-jest/presets/default-esm` for ES modules +- **Test Environment**: Node.js +- **Transform**: TypeScript compilation with decorators +- **Module Resolution**: Support for ES module imports +- **Setup**: Global test configuration in `jest.setup.js` + +## Tips for Writing Tests + +1. **Use descriptive test names**: `it('should return error when input is invalid')` +2. **Group related tests**: Use `describe` blocks to organize tests +3. **Test edge cases**: Include tests for error conditions and boundary values +4. **Keep tests focused**: Each test should verify one specific behavior +5. **Use appropriate matchers**: Choose the most specific Jest matcher for clarity + +## Common Jest Matchers + +```typescript +// Equality +expect(value).toBe(expected); // Strict equality (===) +expect(value).toEqual(expected); // Deep equality + +// Truthiness +expect(value).toBeTruthy(); +expect(value).toBeFalsy(); +expect(value).toBeNull(); +expect(value).toBeUndefined(); +expect(value).toBeDefined(); + +// Numbers +expect(value).toBeGreaterThan(3); +expect(value).toBeCloseTo(0.3); + +// Strings +expect(string).toMatch(/pattern/); +expect(string).toContain('substring'); + +// Arrays and Objects +expect(array).toContain(item); +expect(object).toHaveProperty('key'); +expect(array).toHaveLength(3); + +// Functions +expect(fn).toThrow(); +expect(fn).toHaveBeenCalled(); +expect(fn).toHaveBeenCalledWith(args); +``` + +## Next Steps + +1. **Start testing**: Use the provided template in `src/__tests__/test-template.ts` +2. **Add more tests**: Focus on your core business logic first +3. **Increase coverage**: Aim to gradually increase coverage thresholds +4. **CI Integration**: Use `npm run test:ci` in your CI/CD pipeline + +## Troubleshooting + +### Common Issues + +1. **ES Module errors**: Ensure imports use `.js` extensions in test files if needed +2. **Decorator errors**: Check that `experimentalDecorators` is enabled +3. **Coverage issues**: Verify file paths in `collectCoverageFrom` configuration +4. **Type errors**: Ensure `@types/jest` is installed and imported correctly + +### Getting Help + +- Check the [Jest documentation](https://jestjs.io/docs/getting-started) +- Review the [ts-jest documentation](https://kulshekhar.github.io/ts-jest/) +- Examine existing test files for patterns and examples \ No newline at end of file diff --git a/src/typescript/eslint.config.js b/src/typescript/eslint.config.js new file mode 100644 index 0000000..8c96b83 --- /dev/null +++ b/src/typescript/eslint.config.js @@ -0,0 +1,56 @@ +import js from '@eslint/js'; +import typescript from '@typescript-eslint/eslint-plugin'; +import typescriptParser from '@typescript-eslint/parser'; + +export default [ + js.configs.recommended, + { + files: ['src/**/*.ts'], + languageOptions: { + parser: typescriptParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + } + }, + plugins: { + '@typescript-eslint': typescript + }, + rules: { + 'indent': ['error', 2], + 'quotes': ['error', 'single'], + 'semi': ['error', 'always'], + 'comma-dangle': ['error', 'never'], + 'no-trailing-spaces': 'error', + 'no-multiple-empty-lines': ['error', { 'max': 1 }], + 'eol-last': ['error', 'always'], + '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }] + } + }, + { + files: ['src/**/__tests__/**/*.ts'], + languageOptions: { + globals: { + jest: 'readonly', + describe: 'readonly', + it: 'readonly', + test: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly' + } + } + }, + { + ignores: [ + 'dist/**/*', + 'coverage/**/*', + 'node_modules/**/*', + '*.js', + '*.config.js', + '*.d.ts' + ] + } +]; \ No newline at end of file diff --git a/src/typescript/examples/json-handlers-example.ts b/src/typescript/examples/json-handlers-example.ts new file mode 100644 index 0000000..556cae2 --- /dev/null +++ b/src/typescript/examples/json-handlers-example.ts @@ -0,0 +1,78 @@ +import { PolyBusBuilder, JsonHandlers, MessageType, messageInfo } from '../src/index'; + +// Example message class with metadata +@messageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 0, 0) +class CreateUserCommand { + constructor( + public readonly name: string, + public readonly email: string + ) {} +} + +// Example of configuring JsonHandlers with PolyBus +async function setupJsonHandlers() { + const builder = new PolyBusBuilder(); + + // Create JsonHandlers instance with custom configuration + const jsonHandlers = new JsonHandlers(); + jsonHandlers.contentType = 'application/json'; + jsonHandlers.throwOnMissingType = true; + jsonHandlers.throwOnInvalidType = true; + + // Optional: Configure custom JSON processing + jsonHandlers.jsonReplacer = (key: string, value: any) => { + // Custom serialization logic if needed + if (key === 'password') { + return undefined; // Remove password fields during serialization + } + return value; + }; + + jsonHandlers.jsonReviver = (key: string, value: any) => { + // Custom deserialization logic if needed + if (key === 'createdAt' && typeof value === 'string') { + return new Date(value); // Convert ISO strings to Date objects + } + return value; + }; + + // Add JsonHandlers to the incoming and outgoing pipeline + builder.incomingHandlers.push(jsonHandlers.deserializer.bind(jsonHandlers)); + builder.outgoingHandlers.push(jsonHandlers.serializer.bind(jsonHandlers)); + + // Register message types + builder.messages.add(CreateUserCommand); + + // Build and configure the bus + const bus = await builder.build(); + + return bus; +} + +// Example usage +async function example() { + try { + const bus = await setupJsonHandlers(); + await bus.start(); + + // Create and send a message + const transaction = await bus.createTransaction(); + const command = new CreateUserCommand('John Doe', 'john@example.com'); + transaction.addOutgoingMessage(command); + + await transaction.commit(); + + console.log('Message sent successfully'); + + await bus.stop(); + } catch (error) { + console.error('Error:', error); + } +} + +// Run the example +if (require.main === module) { + example().catch(console.error); +} + +export { setupJsonHandlers, CreateUserCommand, example }; \ No newline at end of file diff --git a/src/typescript/jest.config.js b/src/typescript/jest.config.js new file mode 100644 index 0000000..0aaafae --- /dev/null +++ b/src/typescript/jest.config.js @@ -0,0 +1,75 @@ +/** @type {import('jest').Config} */ +export default { + preset: 'ts-jest/presets/default-esm', + extensionsToTreatAsEsm: ['.ts'], + testEnvironment: 'node', + + // Test file patterns + testMatch: [ + '/src/**/__tests__/**/*.ts', + '/src/**/?(*.)+(spec|test).ts' + ], + + // Ignore compiled output and other directories + testPathIgnorePatterns: [ + '/node_modules/', + '/dist/', + '/coverage/' + ], + + // Coverage configuration + collectCoverage: true, + coverageDirectory: 'coverage', + coverageReporters: [ + 'text', + 'text-summary', + 'html', + 'lcov', + 'json' + ], + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/**/*.test.ts', + '!src/**/*.spec.ts', + '!src/**/index.ts', // Often just re-exports + '!src/**/*template*.ts' // Ignore test templates + ], + coverageThreshold: { + global: { + branches: 50, + functions: 50, + lines: 50, + statements: 50 + } + }, + + // Module resolution - removed invalid moduleNameMapping option + + // Transform configuration + transform: { + '^.+\\.ts$': ['ts-jest', { + useESM: true, + tsconfig: { + target: 'ES2018', + module: 'ESNext', + moduleResolution: 'node', + allowSyntheticDefaultImports: true, + esModuleInterop: true, + experimentalDecorators: true, + emitDecoratorMetadata: true, + strict: true + } + }] + }, + + // Setup files + setupFilesAfterEnv: ['/jest.setup.js'], + + // Clear mocks between tests + clearMocks: true, + restoreMocks: true, + + // Verbose output + verbose: true +}; \ No newline at end of file diff --git a/src/typescript/jest.setup.js b/src/typescript/jest.setup.js new file mode 100644 index 0000000..48d60a0 --- /dev/null +++ b/src/typescript/jest.setup.js @@ -0,0 +1,9 @@ +// Jest setup file for global configurations + +// Enable experimental features if needed +// import 'reflect-metadata'; + +// Global test timeout (30 seconds) +jest.setTimeout(30000); + +// Add any global mocks or configurations here \ No newline at end of file diff --git a/src/typescript/package-lock.json b/src/typescript/package-lock.json new file mode 100644 index 0000000..3821eab --- /dev/null +++ b/src/typescript/package-lock.json @@ -0,0 +1,6404 @@ +{ + "name": "poly-bus", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "poly-bus", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "@eslint/js": "^9.39.0", + "@jest/globals": "^30.2.0", + "@rollup/plugin-typescript": "^11.1.5", + "@types/jest": "^30.0.0", + "@types/node": "^20.8.0", + "@typescript-eslint/eslint-plugin": "^8.46.2", + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^9.39.0", + "jest": "^30.2.0", + "rollup": "^4.1.4", + "ts-jest": "^29.4.5", + "tslib": "^2.6.2", + "typescript": "^5.2.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", + "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", + "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", + "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", + "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", + "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz", + "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001753", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", + "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.244", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint": { + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", + "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.0", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/typescript/package.json b/src/typescript/package.json new file mode 100644 index 0000000..3bbc0f4 --- /dev/null +++ b/src/typescript/package.json @@ -0,0 +1,84 @@ +{ + "name": "poly-bus", + "version": "1.0.0", + "type": "module", + "description": "A TypeScript NPM package that works in both browser and Node.js environments", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "browser": "./dist/index.umd.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js", + "browser": "./dist/index.umd.js" + } + }, + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "npm run build:node && npm run build:browser && npm run build:types", + "build:node": "tsc --module commonjs --outDir dist/cjs && mv dist/cjs/index.js dist/index.js", + "build:browser": "rollup -c", + "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist", + "clean": "rm -rf dist", + "dev": "tsc --watch", + "lint": "eslint src/**/*.ts", + "lint:fix": "eslint src/**/*.ts --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "test:dev": "jest --coverage --passWithNoTests --coverageThreshold='{}'", + "test:ci": "jest --coverage --watchAll=false --passWithNoTests", + "prepublishOnly": "npm run clean && npm run build" + }, + "keywords": [ + "typescript", + "browser", + "node", + "universal", + "dual-package", + "messaging", + "message-bus", + "polyglot", + "microservices", + "distributed-systems" + ], + "author": "Cy Scott ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/CyAScott/poly-bus.git", + "directory": "src/typescript" + }, + "bugs": { + "url": "https://github.com/CyAScott/poly-bus/issues" + }, + "homepage": "https://github.com/CyAScott/poly-bus#readme", + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@eslint/js": "^9.39.0", + "@jest/globals": "^30.2.0", + "@rollup/plugin-typescript": "^11.1.5", + "@types/jest": "^30.0.0", + "@types/node": "^20.8.0", + "@typescript-eslint/eslint-plugin": "^8.46.2", + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^9.39.0", + "jest": "^30.2.0", + "rollup": "^4.1.4", + "ts-jest": "^29.4.5", + "tslib": "^2.6.2", + "typescript": "^5.2.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "dependencies": { + "reflect-metadata": "^0.2.2" + } +} diff --git a/src/typescript/rollup.config.js b/src/typescript/rollup.config.js new file mode 100644 index 0000000..8822dac --- /dev/null +++ b/src/typescript/rollup.config.js @@ -0,0 +1,40 @@ +import typescript from '@rollup/plugin-typescript'; + +export default [ + // ES Module build + { + input: 'src/index.ts', + output: { + file: 'dist/index.mjs', + format: 'es', + sourcemap: true + }, + plugins: [ + typescript({ + tsconfig: './tsconfig.json', + declaration: false, + declarationMap: false + }) + ], + external: [] // Add external dependencies here if any + }, + // UMD build for browsers + { + input: 'src/index.ts', + output: { + file: 'dist/index.umd.js', + format: 'umd', + name: 'PolyBus', + exports: 'named', + sourcemap: true + }, + plugins: [ + typescript({ + tsconfig: './tsconfig.json', + declaration: false, + declarationMap: false + }) + ], + external: [] // Add external dependencies here if any + } +]; \ No newline at end of file diff --git a/src/typescript/src/__tests__/headers.test.ts b/src/typescript/src/__tests__/headers.test.ts new file mode 100644 index 0000000..3704aeb --- /dev/null +++ b/src/typescript/src/__tests__/headers.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from '@jest/globals'; +import { Headers } from '../headers'; + +describe('Headers', () => { + describe('ContentType', () => { + it('should have the correct content-type header name', () => { + expect(Headers.ContentType).toBe('content-type'); + }); + + it('should be a readonly property', () => { + // This test ensures the property is static and readonly + expect(typeof Headers.ContentType).toBe('string'); + expect(Headers.ContentType).toBeDefined(); + }); + }); + + describe('MessageType', () => { + it('should have the correct message type header name', () => { + expect(Headers.MessageType).toBe('x-type'); + }); + + it('should be a readonly property', () => { + // This test ensures the property is static and readonly + expect(typeof Headers.MessageType).toBe('string'); + expect(Headers.MessageType).toBeDefined(); + }); + }); + + describe('Headers class structure', () => { + it('should be a class with static properties', () => { + expect(Headers).toBeDefined(); + expect(typeof Headers).toBe('function'); + }); + + it('should not be instantiable (static class pattern)', () => { + // Since this is a utility class with only static properties, + // we can test that it can be instantiated but serves no purpose + const instance = new Headers(); + expect(instance).toBeDefined(); + expect(instance.constructor).toBe(Headers); + }); + }); +}); diff --git a/src/typescript/src/__tests__/poly-bus.test.ts b/src/typescript/src/__tests__/poly-bus.test.ts new file mode 100644 index 0000000..43ff32f --- /dev/null +++ b/src/typescript/src/__tests__/poly-bus.test.ts @@ -0,0 +1,203 @@ +import { describe, it, expect, beforeEach } from '@jest/globals'; +import { PolyBusBuilder } from '../poly-bus-builder'; +import { IncomingTransaction } from '../transport/transaction/incoming-transaction'; +import { OutgoingTransaction } from '../transport/transaction/outgoing-transaction'; +import { InMemoryTransport } from '../transport/in-memory/in-memory-transport'; + +describe('PolyBus', () => { + let inMemoryTransport: InMemoryTransport; + + beforeEach(() => { + inMemoryTransport = new InMemoryTransport(); + }); + + describe('IncomingHandlers', () => { + it('should invoke incoming handlers', async () => { + // Arrange + const incomingTransactionPromise = new Promise((resolve) => { + const builder = new PolyBusBuilder(); + builder.incomingHandlers.push(async (transaction, next) => { + await next(); + resolve(transaction); + }); + builder.transportFactory = async (builder, bus) => inMemoryTransport.addEndpoint(builder, bus); + + builder.build().then(async (bus) => { + // Act + await bus.start(); + const outgoingTransaction = await bus.createTransaction(); + outgoingTransaction.addOutgoingMessage('Hello world', 'unknown-endpoint'); + await outgoingTransaction.commit(); + + // Allow processing to complete + // eslint-disable-next-line no-undef + await new Promise(resolve => setTimeout(resolve, 10)); + await bus.stop(); + }); + }); + + const transaction = await incomingTransactionPromise; + + // Assert + expect(transaction).toBeDefined(); + expect(transaction.incomingMessage.body).toBe('Hello world'); + }); + + it('should invoke incoming handlers even when exception is thrown', async () => { + // Arrange + const incomingTransactionPromise = new Promise((resolve) => { + const builder = new PolyBusBuilder(); + builder.incomingHandlers.push(async (transaction) => { + resolve(transaction); + throw new Error(transaction.incomingMessage.body); + }); + builder.transportFactory = async (builder, bus) => inMemoryTransport.addEndpoint(builder, bus); + + builder.build().then(async (bus) => { + // Act + await bus.start(); + const outgoingTransaction = await bus.createTransaction(); + outgoingTransaction.addOutgoingMessage('Hello world', 'unknown-endpoint'); + await outgoingTransaction.commit(); + + // Allow processing to complete + // eslint-disable-next-line no-undef + await new Promise(resolve => setTimeout(resolve, 10)); + await bus.stop(); + }); + }); + + const transaction = await incomingTransactionPromise; + + // Assert + expect(transaction).toBeDefined(); + expect(transaction.incomingMessage.body).toBe('Hello world'); + }); + + it('should invoke incoming handlers with delay', async () => { + // Arrange + const processedOnPromise = new Promise((resolve) => { + const builder = new PolyBusBuilder(); + builder.incomingHandlers.push(async (_, next) => { + await next(); + resolve(new Date()); + }); + builder.transportFactory = async (builder, bus) => inMemoryTransport.addEndpoint(builder, bus); + + builder.build().then(async (bus) => { + // Act + await bus.start(); + const outgoingTransaction = await bus.createTransaction(); + const message = outgoingTransaction.addOutgoingMessage('Hello world', 'unknown-endpoint'); + const scheduledAt = new Date(Date.now() + 1000); // 1 second from now + message.deliverAt = scheduledAt; + await outgoingTransaction.commit(); + + // Allow processing to complete + // eslint-disable-next-line no-undef + setTimeout(async () => { + await bus.stop(); + }, 2000); // Stop after 2 seconds + }); + }); + + const processedOn = await processedOnPromise; + + // Assert + expect(processedOn).toBeDefined(); + const now = new Date(); + const timeDiff = processedOn.getTime() - (now.getTime() - 2000); // Approximately when it was scheduled + expect(timeDiff).toBeGreaterThanOrEqual(800); // Allow some margin for timing + }, 5000); // 5 second timeout for this test + + it('should invoke incoming handlers with delay and exception', async () => { + // Arrange + const processedOnPromise = new Promise((resolve) => { + const builder = new PolyBusBuilder(); + builder.incomingHandlers.push(async (transaction) => { + resolve(new Date()); + throw new Error(transaction.incomingMessage.body); + }); + builder.transportFactory = async (builder, bus) => inMemoryTransport.addEndpoint(builder, bus); + + builder.build().then(async (bus) => { + // Act + await bus.start(); + const outgoingTransaction = await bus.createTransaction(); + const message = outgoingTransaction.addOutgoingMessage('Hello world', 'unknown-endpoint'); + const scheduledAt = new Date(Date.now() + 1000); // 1 second from now + message.deliverAt = scheduledAt; + await outgoingTransaction.commit(); + + // Allow processing to complete + // eslint-disable-next-line no-undef + setTimeout(async () => { + await bus.stop(); + }, 2000); // Stop after 2 seconds + }); + }); + + const processedOn = await processedOnPromise; + + // Assert + expect(processedOn).toBeDefined(); + const now = new Date(); + const timeDiff = processedOn.getTime() - (now.getTime() - 2000); // Approximately when it was scheduled + expect(timeDiff).toBeGreaterThanOrEqual(800); // Allow some margin for timing + }, 5000); // 5 second timeout for this test + }); + + describe('OutgoingHandlers', () => { + it('should invoke outgoing handlers', async () => { + // Arrange + const outgoingTransactionPromise = new Promise((resolve) => { + const builder = new PolyBusBuilder(); + builder.outgoingHandlers.push(async (transaction, next) => { + await next(); + resolve(transaction); + }); + builder.transportFactory = async (builder, bus) => inMemoryTransport.addEndpoint(builder, bus); + + builder.build().then(async (bus) => { + // Act + await bus.start(); + const outgoingTransaction = await bus.createTransaction(); + outgoingTransaction.addOutgoingMessage('Hello world', 'unknown-endpoint'); + await outgoingTransaction.commit(); + + // Allow processing to complete + // eslint-disable-next-line no-undef + await new Promise(resolve => setTimeout(resolve, 10)); + await bus.stop(); + }); + }); + + const transaction = await outgoingTransactionPromise; + + // Assert + expect(transaction).toBeDefined(); + expect(transaction.outgoingMessages).toHaveLength(1); + expect(transaction.outgoingMessages[0].body).toBe('Hello world'); + }); + + it('should invoke outgoing handlers and handle exceptions', async () => { + // Arrange + const builder = new PolyBusBuilder(); + builder.outgoingHandlers.push(async (transaction) => { + throw new Error(transaction.outgoingMessages[0].body); + }); + builder.transportFactory = async (builder, bus) => inMemoryTransport.addEndpoint(builder, bus); + + const bus = await builder.build(); + + // Act & Assert + await bus.start(); + const outgoingTransaction = await bus.createTransaction(); + outgoingTransaction.addOutgoingMessage('Hello world', 'unknown-endpoint'); + + await expect(outgoingTransaction.commit()).rejects.toThrow('Hello world'); + + await bus.stop(); + }); + }); +}); diff --git a/src/typescript/src/__tests__/test-template.ts b/src/typescript/src/__tests__/test-template.ts new file mode 100644 index 0000000..a2390ea --- /dev/null +++ b/src/typescript/src/__tests__/test-template.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; + +/** + * Template test file for PolyBus components + * + * Replace 'ComponentName' with your actual component name + * Replace './component-name' with the actual import path + * + * Common Jest matchers: + * - expect(value).toBe(expected) - strict equality (===) + * - expect(value).toEqual(expected) - deep equality + * - expect(value).toBeNull() + * - expect(value).toBeUndefined() + * - expect(value).toBeDefined() + * - expect(value).toBeTruthy() + * - expect(value).toBeFalsy() + * - expect(array).toContain(item) + * - expect(object).toHaveProperty('key') + * - expect(fn).toThrow() + * - expect(fn).toHaveBeenCalled() + * - expect(fn).toHaveBeenCalledWith(args) + */ + +// import { ComponentName } from './component-name'; + +describe('ComponentName', () => { + // Setup and teardown + beforeEach(() => { + // Setup before each test + jest.clearAllMocks(); + }); + + afterEach(() => { + // Cleanup after each test + }); + + describe('constructor', () => { + it('should create an instance', () => { + // const instance = new ComponentName(); + // expect(instance).toBeDefined(); + // expect(instance).toBeInstanceOf(ComponentName); + }); + }); + + describe('public methods', () => { + it('should have the expected behavior', () => { + // Test your public methods here + expect(true).toBe(true); // Placeholder + }); + }); + + describe('error handling', () => { + it('should handle invalid input gracefully', () => { + // Test error cases + expect(true).toBe(true); // Placeholder + }); + }); + + describe('integration tests', () => { + it('should work with other components', () => { + // Test integration with other parts of your system + expect(true).toBe(true); // Placeholder + }); + }); +}); diff --git a/src/typescript/src/headers.ts b/src/typescript/src/headers.ts new file mode 100644 index 0000000..914a141 --- /dev/null +++ b/src/typescript/src/headers.ts @@ -0,0 +1,14 @@ +/** + * Common header names used in PolyBus. + */ +export class Headers { + /** + * The content type header name used for specifying the message content type (e.g., "application/json"). + */ + public static readonly ContentType = 'content-type'; + + /** + * The message type header name used for specifying the type of the message. + */ + public static readonly MessageType = 'x-type'; +} \ No newline at end of file diff --git a/src/typescript/src/i-poly-bus.ts b/src/typescript/src/i-poly-bus.ts new file mode 100644 index 0000000..c05aab1 --- /dev/null +++ b/src/typescript/src/i-poly-bus.ts @@ -0,0 +1,64 @@ +import { IncomingHandler } from './transport/transaction/message/handlers/incoming-handler'; +import { IncomingMessage } from './transport/transaction/message/incoming-message'; +import { ITransport } from './transport/i-transport'; +import { Messages } from './transport/transaction/message/messages'; +import { OutgoingHandler } from './transport/transaction/message/handlers/outgoing-handler'; +import { Transaction } from './transport/transaction/transaction'; + +export interface IPolyBus { + /** + * The properties associated with this bus instance. + */ + properties: Map; + + /** + * The transport mechanism used by this bus instance. + */ + transport: ITransport; + + /** + * Collection of handlers for processing incoming messages. + */ + incomingHandlers: IncomingHandler[]; + + /** + * Collection of handlers for processing outgoing messages. + */ + outgoingHandlers: OutgoingHandler[]; + + /** + * Collection of message types and their associated headers. + */ + messages: Messages; + + /** + * Creates a new transaction, optionally based on an incoming message. + * @param message Optional incoming message to create the transaction from. + * @returns A promise that resolves to the created transaction. + */ + createTransaction(message?: IncomingMessage): Promise; + + /** + * Sends messages associated with the given transaction to the transport. + * @param transaction The transaction containing messages to send. + * @returns A promise that resolves when the messages have been sent. + */ + send(transaction: Transaction): Promise; + + /** + * Starts the bus and begins processing messages. + * @returns A promise that resolves when the bus has started. + */ + start(): Promise; + + /** + * Stops the bus and ceases processing messages. + * @returns A promise that resolves when the bus has stopped. + */ + stop(): Promise; + + /** + * The name of this bus instance. + */ + name: string; +} diff --git a/src/typescript/src/index.ts b/src/typescript/src/index.ts new file mode 100644 index 0000000..af4088c --- /dev/null +++ b/src/typescript/src/index.ts @@ -0,0 +1,28 @@ +/** + * poly-bus - A TypeScript NPM package for both browser and Node.js environments + */ + +// Import reflect-metadata for decorator support +import 'reflect-metadata'; + +// Export message-related types and decorators +export { ErrorHandler } from './transport/transaction/message/handlers/error/error-handlers'; +export { Headers } from './headers'; +export { IncomingHandler } from './transport/transaction/message/handlers/incoming-handler'; +export { IncomingMessage } from './transport/transaction/message/incoming-message'; +export { IncomingTransaction } from './transport/transaction/incoming-transaction'; +export { InMemoryTransport } from './transport/in-memory/in-memory-transport'; +export { ITransport } from './transport/i-transport'; +export { IPolyBus } from './i-poly-bus'; +export { JsonHandlers } from './transport/transaction/message/handlers/serializers/json-handlers'; +export { Message } from './transport/transaction/message/message'; +export { MessageInfo, messageInfo, MessageType } from './transport/transaction/message/message-info'; +export { Messages } from './transport/transaction/message/messages'; +export { OutgoingHandler } from './transport/transaction/message/handlers/outgoing-handler'; +export { OutgoingMessage } from './transport/transaction/message/outgoing-message'; +export { OutgoingTransaction } from './transport/transaction/outgoing-transaction'; +export { PolyBus } from './poly-bus'; +export { PolyBusBuilder } from './poly-bus-builder'; +export { Transaction } from './transport/transaction/transaction'; +export { TransactionFactory } from './transport/transaction/transaction-factory'; +export { TransportFactory } from './poly-bus-builder'; diff --git a/src/typescript/src/poly-bus-builder.ts b/src/typescript/src/poly-bus-builder.ts new file mode 100644 index 0000000..94bef34 --- /dev/null +++ b/src/typescript/src/poly-bus-builder.ts @@ -0,0 +1,120 @@ + +import { IPolyBus } from './i-poly-bus'; +import { ITransport } from './transport/i-transport'; +import { IncomingHandler } from './transport/transaction/message/handlers/incoming-handler'; +import { OutgoingHandler } from './transport/transaction/message/handlers/outgoing-handler'; +import { Messages } from './transport/transaction/message/messages'; +import { TransactionFactory } from './transport/transaction/transaction-factory'; +import { IncomingTransaction } from './transport/transaction/incoming-transaction'; +import { OutgoingTransaction } from './transport/transaction/outgoing-transaction'; +import { PolyBus } from './poly-bus'; + +/** + * A factory method for creating the transport for the PolyBus instance. + * The transport is responsible for sending and receiving messages. + */ +export type TransportFactory = (builder: PolyBusBuilder, bus: IPolyBus) => Promise; + +/** + * Builder class for configuring and creating PolyBus instances. + */ +export class PolyBusBuilder { + private _transactionFactory: TransactionFactory; + private _transportFactory: TransportFactory; + private readonly _incomingHandlers: IncomingHandler[] = []; + private readonly _outgoingHandlers: OutgoingHandler[] = []; + private readonly _messages: Messages = new Messages(); + private _name: string = 'PolyBusInstance'; + + /** + * Creates a new PolyBusBuilder instance. + */ + constructor() { + // Default transaction factory - creates IncomingTransaction for incoming messages, + // OutgoingTransaction for outgoing messages + this._transactionFactory = (_, bus, message) => { + return Promise.resolve( + message != null + ? new IncomingTransaction(bus, message) + : new OutgoingTransaction(bus) + ); + }; + + // Default transport factory - should be overridden by specific transport implementations + this._transportFactory = async (_builder, _bus) => { + throw new Error('Transport factory must be configured before building PolyBus. Use a transport-specific builder method.'); + }; + } + + /** + * The transaction factory will be used to create transactions for message handling. + * Transactions are used to ensure that a group of messages related to a single request + * are sent to the transport in a single atomic operation. + */ + public get transactionFactory(): TransactionFactory { + return this._transactionFactory; + } + + public set transactionFactory(value: TransactionFactory) { + this._transactionFactory = value; + } + + /** + * The transport factory will be used to create the transport for the PolyBus instance. + * The transport is responsible for sending and receiving messages. + */ + public get transportFactory(): TransportFactory { + return this._transportFactory; + } + + public set transportFactory(value: TransportFactory) { + this._transportFactory = value; + } + + /** + * The properties associated with this bus instance. + */ + properties: Map = new Map(); + + /** + * Collection of handlers for processing incoming messages. + */ + public get incomingHandlers(): IncomingHandler[] { + return this._incomingHandlers; + } + + /** + * Collection of handlers for processing outgoing messages. + */ + public get outgoingHandlers(): OutgoingHandler[] { + return this._outgoingHandlers; + } + + /** + * Collection of message types and their associated headers. + */ + public get messages(): Messages { + return this._messages; + } + + /** + * The name of this bus instance. + */ + public get name(): string { + return this._name; + } + + public set name(value: string) { + this._name = value; + } + + /** + * Builds and configures a new PolyBus instance. + * @returns A promise that resolves to the configured PolyBus instance. + */ + public async build(): Promise { + const bus = new PolyBus(this); + bus.transport = await this._transportFactory(this, bus); + return bus; + } +} diff --git a/src/typescript/src/poly-bus.ts b/src/typescript/src/poly-bus.ts new file mode 100644 index 0000000..6ae72d2 --- /dev/null +++ b/src/typescript/src/poly-bus.ts @@ -0,0 +1,138 @@ +import { IPolyBus } from './i-poly-bus'; +import { ITransport } from './transport/i-transport'; +import { IncomingHandler } from './transport/transaction/message/handlers/incoming-handler'; +import { OutgoingHandler } from './transport/transaction/message/handlers/outgoing-handler'; +import { Messages } from './transport/transaction/message/messages'; +import { Transaction } from './transport/transaction/transaction'; +import { IncomingTransaction } from './transport/transaction/incoming-transaction'; +import { OutgoingTransaction } from './transport/transaction/outgoing-transaction'; +import { IncomingMessage } from './transport/transaction/message/incoming-message'; +import { PolyBusBuilder } from './poly-bus-builder'; + +/** + * Implementation of IPolyBus that provides message handling and transport functionality. + */ +export class PolyBus implements IPolyBus { + private _transport!: ITransport; + private readonly _incomingHandlers: IncomingHandler[]; + private readonly _outgoingHandlers: OutgoingHandler[]; + private readonly _messages: Messages; + private readonly _name: string; + private readonly _builder: PolyBusBuilder; + + /** + * Creates a new PolyBus instance. + * @param builder The builder containing configuration for this bus instance. + */ + constructor(builder: PolyBusBuilder) { + this._builder = builder; + this._incomingHandlers = builder.incomingHandlers; + this._outgoingHandlers = builder.outgoingHandlers; + this._messages = builder.messages; + this._name = builder.name; + } + + /** + * The properties associated with this bus instance. + */ + public get properties(): Map { + return this._builder.properties; + } + + /** + * The transport mechanism used by this bus instance. + */ + public get transport(): ITransport { + return this._transport; + } + + public set transport(value: ITransport) { + this._transport = value; + } + + /** + * Collection of handlers for processing incoming messages. + */ + public get incomingHandlers(): IncomingHandler[] { + return this._incomingHandlers; + } + + /** + * Collection of handlers for processing outgoing messages. + */ + public get outgoingHandlers(): OutgoingHandler[] { + return this._outgoingHandlers; + } + + /** + * Collection of message types and their associated headers. + */ + public get messages(): Messages { + return this._messages; + } + + /** + * The name of this bus instance. + */ + public get name(): string { + return this._name; + } + + /** + * Creates a new transaction, optionally based on an incoming message. + * @param message Optional incoming message to create the transaction from. + * @returns A promise that resolves to the created transaction. + */ + public async createTransaction(message?: IncomingMessage): Promise { + return this._builder.transactionFactory(this._builder, this, message); + } + + /** + * Sends messages associated with the given transaction to the transport. + * Applies appropriate handlers based on transaction type before sending. + * @param transaction The transaction containing messages to send. + * @returns A promise that resolves when the messages have been sent. + */ + public async send(transaction: Transaction): Promise { + let step = () => this.transport.send(transaction); + + if (transaction instanceof IncomingTransaction) { + const handlers = transaction.bus.incomingHandlers; + for (let index = handlers.length - 1; index >= 0; index--) { + const handler = handlers[index]; + const next = step; + step = () => handler(transaction, next); + } + } else if (transaction instanceof OutgoingTransaction) { + const handlers = transaction.bus.outgoingHandlers; + for (let index = handlers.length - 1; index >= 0; index--) { + const handler = handlers[index]; + const next = step; + step = () => handler(transaction, next); + } + } + + try { + await step(); + } catch (error) { + await transaction.abort(); + throw error; + } + } + + /** + * Starts the bus and begins processing messages. + * @returns A promise that resolves when the bus has started. + */ + public async start(): Promise { + return this.transport.start(); + } + + /** + * Stops the bus and ceases processing messages. + * @returns A promise that resolves when the bus has stopped. + */ + public async stop(): Promise { + return this.transport.stop(); + } +} diff --git a/src/typescript/src/transport/i-transport.ts b/src/typescript/src/transport/i-transport.ts new file mode 100644 index 0000000..2765093 --- /dev/null +++ b/src/typescript/src/transport/i-transport.ts @@ -0,0 +1,36 @@ +import { MessageInfo } from './transaction/message/message-info'; +import { Transaction } from './transaction/transaction'; + +/** + * An interface for a transport mechanism to send and receive messages. + */ +export interface ITransport { + + supportsCommandMessages: boolean; + + supportsDelayedMessages: boolean; + + supportsSubscriptions: boolean; + + /** + * Sends messages associated with the given transaction to the transport. + */ + // eslint-disable-next-line no-unused-vars + send(transaction: Transaction): Promise; + + /** + * Subscribes to a messages so that the transport can start receiving them. + */ + // eslint-disable-next-line no-unused-vars + subscribe(messageInfo: MessageInfo): Promise; + + /** + * Enables the transport to start processing messages. + */ + start(): Promise; + + /** + * Stops the transport from processing messages. + */ + stop(): Promise; +} diff --git a/src/typescript/src/transport/in-memory/in-memory-transport.ts b/src/typescript/src/transport/in-memory/in-memory-transport.ts new file mode 100644 index 0000000..c5e10a4 --- /dev/null +++ b/src/typescript/src/transport/in-memory/in-memory-transport.ts @@ -0,0 +1,146 @@ +import { IncomingMessage } from '../transaction/message/incoming-message'; +import { IncomingTransaction } from '../transaction/incoming-transaction'; +import { IPolyBus } from '../../i-poly-bus'; +import { ITransport } from '../i-transport'; +import { MessageInfo } from '../transaction/message/message-info'; +import { OutgoingMessage } from '../transaction/message/outgoing-message'; +import { PolyBusBuilder } from '../../poly-bus-builder'; +import { Transaction } from '../transaction/transaction'; + +export class InMemoryTransport { + public addEndpoint(_builder: PolyBusBuilder, bus: IPolyBus): ITransport { + const endpoint = new Endpoint(this, bus); + this._endpoints.set(bus.name, endpoint); + return endpoint; + } + private readonly _endpoints = new Map(); + + public async send(transaction: Transaction): Promise { + if (!this._active) { + throw new Error('Transport is not active.'); + } + + if (transaction.outgoingMessages.length === 0) { + return; + } + + let promiseResolver: () => void = () => {}; + const transactionId = Math.random().toString(36).substring(2, 11); + const promise = new Promise((resolve) => { + promiseResolver = resolve; + }); + this._transactions.set(transactionId, promise); + + try { + const tasks: Promise[] = []; + + for (const message of transaction.outgoingMessages) { + if (message.deliverAt) { + let delay = message.deliverAt.getTime() - Date.now(); + if (delay > 0) { + // eslint-disable-next-line no-undef + const timeoutId = setTimeout(async () => { + try { + const transaction = await message.bus.createTransaction(); + message.deliverAt = undefined; + transaction.outgoingMessages.push(message); + await transaction.commit(); + } catch (error) { + // eslint-disable-next-line no-undef + console.error('Error delivering delayed message:', error); + } finally { + this._timeouts.delete(transactionId); + } + }, delay); + this._timeouts.set(transactionId, timeoutId); + continue; + } + } + for (const endpoint of this._endpoints.values()) { + const task = endpoint.handle(message); + tasks.push(task); + } + } + + await Promise.all(tasks); + } finally { + promiseResolver(); + this._transactions.delete(transactionId); + } + } + // eslint-disable-next-line no-undef + private readonly _timeouts = new Map(); + + public useSubscriptions: boolean = false; + + public async start(): Promise { + this._active = true; + } + private _active: boolean = false; + + public async stop(): Promise { + this._active = false; + for (const timeoutId of this._timeouts.values()) { + // eslint-disable-next-line no-undef + clearTimeout(timeoutId); + } + this._timeouts.clear(); + await Promise.all(this._transactions.values()); + this._transactions.clear(); + } + private readonly _transactions = new Map>(); +} + +class Endpoint implements ITransport { + constructor( + private readonly transport: InMemoryTransport, + private readonly bus: IPolyBus + ) {} + + public async handle(message: OutgoingMessage): Promise { + if (!this.transport.useSubscriptions || this._subscriptions.includes(message.messageType)) { + const incomingMessage = new IncomingMessage(this.bus, message.body); + incomingMessage.headers = message.headers; + + try { + const transaction = await this.bus.createTransaction(incomingMessage) as IncomingTransaction; + await transaction.commit(); + } catch (error) { + console.error('Error processing message:', error); + } + } + } + + private readonly _subscriptions: Function[] = []; + public async subscribe(messageInfo: MessageInfo): Promise { + const type = this.bus.messages.getTypeByMessageInfo(messageInfo); + if (!type) { + throw new Error(`Message type for attribute ${messageInfo.toString()} is not registered.`); + } + this._subscriptions.push(type); + } + + public get supportsCommandMessages(): boolean { + return true; + } + + public get supportsDelayedMessages(): boolean { + return true; + } + + public get supportsSubscriptions(): boolean { + return true; + } + + public async send(transaction: Transaction): Promise { + return this.transport.send(transaction); + } + + public async start(): Promise { + return this.transport.start(); + } + + public async stop(): Promise { + return this.transport.stop(); + } +} diff --git a/src/typescript/src/transport/transaction/incoming-transaction.ts b/src/typescript/src/transport/transaction/incoming-transaction.ts new file mode 100644 index 0000000..e3beff7 --- /dev/null +++ b/src/typescript/src/transport/transaction/incoming-transaction.ts @@ -0,0 +1,33 @@ +import { IncomingMessage } from './message/incoming-message'; +import { IPolyBus } from '../../i-poly-bus'; +import { Transaction } from './transaction'; + +/** + * Represents an incoming transaction in the transport layer. + * Contains the incoming message from the transport being processed. + */ +export class IncomingTransaction extends Transaction { + private readonly _incomingMessage: IncomingMessage; + + /** + * Creates a new IncomingTransaction instance. + * @param bus The bus instance associated with the transaction. + * @param incomingMessage The incoming message from the transport being processed. + */ + constructor(bus: IPolyBus, incomingMessage: IncomingMessage) { + super(bus); + + if (!incomingMessage) { + throw new Error('IncomingMessage parameter cannot be null or undefined'); + } + + this._incomingMessage = incomingMessage; + } + + /** + * The incoming message from the transport being processed. + */ + public get incomingMessage(): IncomingMessage { + return this._incomingMessage; + } +} diff --git a/src/typescript/src/transport/transaction/message/__tests__/message-info.test.ts b/src/typescript/src/transport/transaction/message/__tests__/message-info.test.ts new file mode 100644 index 0000000..a4f614b --- /dev/null +++ b/src/typescript/src/transport/transaction/message/__tests__/message-info.test.ts @@ -0,0 +1,302 @@ +import { describe, it, expect } from '@jest/globals'; +import { MessageInfo } from '../message-info'; +import { MessageType } from '../message-type'; + +describe('MessageInfo', () => { + describe('getAttributeFromHeader', () => { + it('should parse valid header and return correct attribute', () => { + // Arrange + const header = 'endpoint=user-service, type=Command, name=CreateUser, version=1.2.3'; + + // Act + const result = MessageInfo.getAttributeFromHeader(header); + + // Assert + expect(result).not.toBeNull(); + expect(result!.endpoint).toBe('user-service'); + expect(result!.type).toBe(MessageType.Command); + expect(result!.name).toBe('CreateUser'); + expect(result!.major).toBe(1); + expect(result!.minor).toBe(2); + expect(result!.patch).toBe(3); + }); + + it('should parse event type header correctly', () => { + // Arrange + const header = 'endpoint=notification-service, type=Event, name=UserCreated, version=2.0.1'; + + // Act + const result = MessageInfo.getAttributeFromHeader(header); + + // Assert + expect(result).not.toBeNull(); + expect(result!.endpoint).toBe('notification-service'); + expect(result!.type).toBe(MessageType.Event); + expect(result!.name).toBe('UserCreated'); + expect(result!.major).toBe(2); + expect(result!.minor).toBe(0); + expect(result!.patch).toBe(1); + }); + + it('should handle extra spaces correctly', () => { + // Arrange - the current regex doesn't handle spaces within values well, so testing valid spacing + const header = 'endpoint=payment-service, type=Command, name=ProcessPayment, version=3.14.159'; + + // Act + const result = MessageInfo.getAttributeFromHeader(header); + + // Assert + expect(result).not.toBeNull(); + expect(result!.endpoint).toBe('payment-service'); + expect(result!.type).toBe(MessageType.Command); + expect(result!.name).toBe('ProcessPayment'); + expect(result!.major).toBe(3); + expect(result!.minor).toBe(14); + expect(result!.patch).toBe(159); + }); + + it('should handle case insensitive type correctly', () => { + // Arrange + const header = 'endpoint=order-service, type=command, name=PlaceOrder, version=1.0.0'; + + // Act + const result = MessageInfo.getAttributeFromHeader(header); + + // Assert + expect(result).not.toBeNull(); + expect(result!.type).toBe(MessageType.Command); + }); + + it.each([ + [''], + ['invalid header'], + ['endpoint=test'], + ['endpoint=test, type=Command'], + ['endpoint=test, type=Command, name=Test'], + ['endpoint=test, type=Command, name=Test, version=invalid'], + ['type=Command, name=Test, version=1.0.0'] + ])('should return null for invalid header: "%s"', (header) => { + // Act + const result = MessageInfo.getAttributeFromHeader(header); + + // Assert + expect(result).toBeNull(); + }); + + it('should return null for invalid enum type', () => { + // Arrange + const header = 'endpoint=test, type=InvalidType, name=Test, version=1.0.0'; + + // Act + const result = MessageInfo.getAttributeFromHeader(header); + + // Assert + expect(result).toBeNull(); + }); + + it('should return null for missing version', () => { + // Arrange + const header = 'endpoint=test-service, type=Command, name=TestCommand, version='; + + // Act + const result = MessageInfo.getAttributeFromHeader(header); + + // Assert + expect(result).toBeNull(); + }); + + it('should return null for incomplete version', () => { + // Arrange + const header = 'endpoint=test-service, type=Command, name=TestCommand, version=1.0'; + + // Act + const result = MessageInfo.getAttributeFromHeader(header); + + // Assert + expect(result).toBeNull(); + }); + }); + + describe('equals', () => { + it('should return true for identical attributes', () => { + // Arrange + const attr1 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + const attr2 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + + // Act & Assert + expect(attr1.equals(attr2)).toBe(true); + expect(attr2.equals(attr1)).toBe(true); + }); + + it('should return true for same object', () => { + // Arrange + const attr = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + + // Act & Assert + expect(attr.equals(attr)).toBe(true); + }); + + it('should return false for different type', () => { + // Arrange + const attr1 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + const attr2 = new MessageInfo(MessageType.Event, 'user-service', 'CreateUser', 1, 2, 3); + + // Act & Assert + expect(attr1.equals(attr2)).toBe(false); + }); + + it('should return false for different endpoint', () => { + // Arrange + const attr1 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + const attr2 = new MessageInfo(MessageType.Command, 'order-service', 'CreateUser', 1, 2, 3); + + // Act & Assert + expect(attr1.equals(attr2)).toBe(false); + }); + + it('should return false for different name', () => { + // Arrange + const attr1 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + const attr2 = new MessageInfo(MessageType.Command, 'user-service', 'UpdateUser', 1, 2, 3); + + // Act & Assert + expect(attr1.equals(attr2)).toBe(false); + }); + + it('should return false for different major version', () => { + // Arrange + const attr1 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + const attr2 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 2, 2, 3); + + // Act & Assert + expect(attr1.equals(attr2)).toBe(false); + }); + + it('should return true for different minor version', () => { + // Arrange + const attr1 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + const attr2 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 3, 3); + + // Act & Assert + expect(attr1.equals(attr2)).toBe(true); + }); + + it('should return true for different patch version', () => { + // Arrange + const attr1 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + const attr2 = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 4); + + // Act & Assert + expect(attr1.equals(attr2)).toBe(true); + }); + + it('should return false for null object', () => { + // Arrange + const attr = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + + // Act & Assert + expect(attr.equals(null)).toBe(false); + }); + }); + + describe('toString', () => { + it('should serialize with version by default', () => { + // Arrange + const attr = new MessageInfo(MessageType.Command, 'user-service', 'CreateUser', 1, 2, 3); + + // Act + const result = attr.toString(); + + // Assert + expect(result).toBe('endpoint=user-service, type=command, name=CreateUser, version=1.2.3'); + }); + + it('should serialize with version when explicitly requested', () => { + // Arrange + const attr = new MessageInfo(MessageType.Event, 'notification-service', 'UserCreated', 2, 0, 1); + + // Act + const result = attr.toString(true); + + // Assert + expect(result).toBe('endpoint=notification-service, type=event, name=UserCreated, version=2.0.1'); + }); + + it('should serialize without version when requested', () => { + // Arrange + const attr = new MessageInfo(MessageType.Command, 'payment-service', 'ProcessPayment', 3, 14, 159); + + // Act + const result = attr.toString(false); + + // Assert + expect(result).toBe('endpoint=payment-service, type=command, name=ProcessPayment'); + }); + }); + + describe('constructor', () => { + it('should create instance with correct properties', () => { + // Arrange & Act + const attr = new MessageInfo(MessageType.Event, 'test-service', 'TestEvent', 1, 2, 3); + + // Assert + expect(attr.type).toBe(MessageType.Event); + expect(attr.endpoint).toBe('test-service'); + expect(attr.name).toBe('TestEvent'); + expect(attr.major).toBe(1); + expect(attr.minor).toBe(2); + expect(attr.patch).toBe(3); + }); + + it('should create readonly properties', () => { + // Arrange + const attr = new MessageInfo(MessageType.Command, 'test-service', 'TestCommand', 1, 0, 0); + + // Act & Assert - these should not throw in TypeScript with readonly properties + expect(() => { + // These lines would cause compilation errors in TypeScript due to readonly properties + // but we can test that the properties exist and have the expected values + expect(attr.type).toBe(MessageType.Command); + expect(attr.endpoint).toBe('test-service'); + expect(attr.name).toBe('TestCommand'); + expect(attr.major).toBe(1); + expect(attr.minor).toBe(0); + expect(attr.patch).toBe(0); + }).not.toThrow(); + }); + }); + + describe('getMetadata', () => { + it('should return null when no metadata is set', () => { + // Arrange + class TestClass {} + + // Act + const result = MessageInfo.getMetadata(TestClass); + + // Assert + expect(result).toBeNull(); + }); + + it('should return metadata when set via decorator', () => { + // Arrange + @messageInfo(MessageType.Command, 'test-service', 'TestCommand', 1, 2, 3) + class TestClass {} + + // Act + const result = MessageInfo.getMetadata(TestClass); + + // Assert + expect(result).not.toBeNull(); + expect(result!.type).toBe(MessageType.Command); + expect(result!.endpoint).toBe('test-service'); + expect(result!.name).toBe('TestCommand'); + expect(result!.major).toBe(1); + expect(result!.minor).toBe(2); + expect(result!.patch).toBe(3); + }); + }); +}); + +// Import the decorator function for testing +import { messageInfo } from '../message-info'; \ No newline at end of file diff --git a/src/typescript/src/transport/transaction/message/__tests__/messages.test.ts b/src/typescript/src/transport/transaction/message/__tests__/messages.test.ts new file mode 100644 index 0000000..775b259 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/__tests__/messages.test.ts @@ -0,0 +1,243 @@ +import { describe, it, expect, beforeEach } from '@jest/globals'; +import { Messages } from '../messages'; +import { MessageInfo, messageInfo } from '../message-info'; +import { MessageType } from '../message-type'; + +describe('Messages', () => { + let messages: Messages; + + beforeEach(() => { + messages = new Messages(); + }); + + // Test Message Classes + @messageInfo(MessageType.Command, 'OrderService', 'CreateOrder', 1, 0, 0) + class CreateOrderCommand { + public orderId: string = ''; + public amount: number = 0; + } + + @messageInfo(MessageType.Event, 'OrderService', 'OrderCreated', 2, 1, 3) + class OrderCreatedEvent { + public orderId: string = ''; + public createdAt: Date = new Date(); + } + + @messageInfo(MessageType.Command, 'PaymentService', 'ProcessPayment', 1, 5, 2) + class ProcessPaymentCommand { + public paymentId: string = ''; + public amount: number = 0; + } + + class MessageWithoutAttribute { + public data: string = ''; + } + + describe('add', () => { + it('should return MessageInfo for valid message type', () => { + // Act + const result = messages.add(CreateOrderCommand); + + // Assert + expect(result).not.toBeNull(); + expect(result.type).toBe(MessageType.Command); + expect(result.endpoint).toBe('OrderService'); + expect(result.name).toBe('CreateOrder'); + expect(result.major).toBe(1); + expect(result.minor).toBe(0); + expect(result.patch).toBe(0); + }); + + it('should throw error for message type without attribute', () => { + // Act & Assert + expect(() => messages.add(MessageWithoutAttribute)).toThrow(); + expect(() => messages.add(MessageWithoutAttribute)).toThrow(/does not have MessageInfo metadata/); + expect(() => messages.add(MessageWithoutAttribute)).toThrow(/MessageWithoutAttribute/); + }); + + it('should throw error when adding same type twice', () => { + // Arrange + messages.add(CreateOrderCommand); + + // Act & Assert + expect(() => messages.add(CreateOrderCommand)).toThrow(); + }); + }); + + describe('getMessageInfo', () => { + it('should return correct MessageInfo for existing type', () => { + // Arrange + messages.add(CreateOrderCommand); + + // Act + const result = messages.getMessageInfo(CreateOrderCommand); + + // Assert + expect(result).not.toBeNull(); + expect(result!.type).toBe(MessageType.Command); + expect(result!.endpoint).toBe('OrderService'); + expect(result!.name).toBe('CreateOrder'); + }); + + it('should return null for non-existent type', () => { + // Act + const result = messages.getMessageInfo(CreateOrderCommand); + + // Assert + expect(result).toBeNull(); + }); + }); + + describe('getHeader', () => { + it('should return correct header for existing type', () => { + // Arrange + messages.add(OrderCreatedEvent); + + // Act + const result = messages.getHeader(OrderCreatedEvent); + + // Assert + expect(result).toBe('endpoint=OrderService, type=event, name=OrderCreated, version=2.1.3'); + }); + + it('should return null for non-existent type', () => { + // Act + const result = messages.getHeader(CreateOrderCommand); + + // Assert + expect(result).toBeNull(); + }); + }); + + describe('getTypeByHeader', () => { + it('should return correct type for valid header', () => { + // Arrange + messages.add(ProcessPaymentCommand); + const header = 'endpoint=PaymentService, type=Command, name=ProcessPayment, version=1.5.2'; + + // Act + const result = messages.getTypeByHeader(header); + + // Assert + expect(result).toBe(ProcessPaymentCommand); + }); + + it('should return null for invalid header', () => { + // Arrange + const invalidHeader = 'invalid header format'; + + // Act + const result = messages.getTypeByHeader(invalidHeader); + + // Assert + expect(result).toBeNull(); + }); + + it('should return null for non-existent message', () => { + // Arrange + const header = 'endpoint=UnknownService, type=Command, name=UnknownCommand, version=1.0.0'; + + // Act + const result = messages.getTypeByHeader(header); + + // Assert + expect(result).toBeNull(); + }); + + it('should cache results', () => { + // Arrange + messages.add(CreateOrderCommand); + const header = 'endpoint=OrderService, type=Command, name=CreateOrder, version=1.0.0'; + + // Act + const result1 = messages.getTypeByHeader(header); + const result2 = messages.getTypeByHeader(header); + + // Assert + expect(result1).toBe(CreateOrderCommand); + expect(result2).toBe(CreateOrderCommand); + expect(result1).toBe(result2); // Reference equality + }); + }); + + describe('getTypeByMessageInfo', () => { + it('should return correct type for existing MessageInfo', () => { + // Arrange + messages.add(OrderCreatedEvent); + const messageInfo = new MessageInfo(MessageType.Event, 'OrderService', 'OrderCreated', 2, 1, 3); + + // Act + const result = messages.getTypeByMessageInfo(messageInfo); + + // Assert + expect(result).toBe(OrderCreatedEvent); + }); + + it('should return null for non-existent MessageInfo', () => { + // Arrange + const messageInfo = new MessageInfo(MessageType.Command, 'UnknownService', 'UnknownCommand', 1, 0, 0); + + // Act + const result = messages.getTypeByMessageInfo(messageInfo); + + // Assert + expect(result).toBeNull(); + }); + + it('should return type for different minor/patch versions', () => { + // Arrange + messages.add(OrderCreatedEvent); // Has version 2.1.3 + const messageInfoDifferentMinor = new MessageInfo(MessageType.Event, 'OrderService', 'OrderCreated', 2, 5, 3); + const messageInfoDifferentPatch = new MessageInfo(MessageType.Event, 'OrderService', 'OrderCreated', 2, 1, 9); + + // Act + const result1 = messages.getTypeByMessageInfo(messageInfoDifferentMinor); + const result2 = messages.getTypeByMessageInfo(messageInfoDifferentPatch); + + // Assert + expect(result1).toBe(OrderCreatedEvent); + expect(result2).toBe(OrderCreatedEvent); + }); + + it('should return null for different major version', () => { + // Arrange + messages.add(OrderCreatedEvent); // Has version 2.1.3 + const messageInfoDifferentMajor = new MessageInfo(MessageType.Event, 'OrderService', 'OrderCreated', 3, 1, 3); + + // Act + const result = messages.getTypeByMessageInfo(messageInfoDifferentMajor); + + // Assert + expect(result).toBeNull(); + }); + }); + + describe('multiple messages integration', () => { + it('should work correctly with multiple messages', () => { + // Arrange + messages.add(CreateOrderCommand); + messages.add(OrderCreatedEvent); + messages.add(ProcessPaymentCommand); + + // Act & Assert - getMessageInfo + const commandInfo = messages.getMessageInfo(CreateOrderCommand); + const eventInfo = messages.getMessageInfo(OrderCreatedEvent); + const paymentInfo = messages.getMessageInfo(ProcessPaymentCommand); + + expect(commandInfo!.type).toBe(MessageType.Command); + expect(eventInfo!.type).toBe(MessageType.Event); + expect(paymentInfo!.endpoint).toBe('PaymentService'); + + // Act & Assert - getHeader + const commandHeader = messages.getHeader(CreateOrderCommand); + const eventHeader = messages.getHeader(OrderCreatedEvent); + + expect(commandHeader).toContain('OrderService'); + expect(eventHeader).toContain('OrderCreated'); + + // Act & Assert - getTypeByHeader + const typeFromHeader = messages.getTypeByHeader(commandHeader!); + expect(typeFromHeader).toBe(CreateOrderCommand); + }); + }); +}); diff --git a/src/typescript/src/transport/transaction/message/handlers/error/__tests__/error-handlers.test.ts b/src/typescript/src/transport/transaction/message/handlers/error/__tests__/error-handlers.test.ts new file mode 100644 index 0000000..dc6b763 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/handlers/error/__tests__/error-handlers.test.ts @@ -0,0 +1,440 @@ +import { describe, it, expect, beforeEach } from '@jest/globals'; +import { ErrorHandler } from '../error-handlers'; +import { IncomingTransaction } from '../../../../incoming-transaction'; +import { IncomingMessage } from '../../../incoming-message'; +import { IPolyBus } from '../../../../../../i-poly-bus'; +import { ITransport } from '../../../../../i-transport'; +import { Messages } from '../../../messages'; +import { IncomingHandler } from '../../incoming-handler'; +import { OutgoingHandler } from '../../outgoing-handler'; +import { Transaction } from '../../../../transaction'; +import { OutgoingTransaction } from '../../../../outgoing-transaction'; + +describe('ErrorHandler', () => { + let testBus: IPolyBus; + let incomingMessage: IncomingMessage; + let transaction: IncomingTransaction; + let errorHandler: TestableErrorHandler; + + beforeEach(() => { + testBus = new TestBus('TestBus'); + incomingMessage = new IncomingMessage(testBus, 'test message body'); + transaction = new IncomingTransaction(testBus, incomingMessage); + errorHandler = new TestableErrorHandler(); + }); + + describe('retrier method', () => { + it('should succeed on first attempt and not retry', async () => { + // Arrange + let nextCalled = false; + const next = async (): Promise => { + nextCalled = true; + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(nextCalled).toBe(true); + expect(transaction.outgoingMessages).toHaveLength(0); + }); + + it('should fail once and retry immediately', async () => { + // Arrange + let callCount = 0; + const next = async (): Promise => { + callCount++; + if (callCount === 1) { + throw new Error('Test error'); + } + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(callCount).toBe(2); + expect(transaction.outgoingMessages).toHaveLength(0); + }); + + it('should fail all immediate retries and schedule delayed retry', async () => { + // Arrange + const expectedRetryTime = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes from now + errorHandler.setNextRetryTime(expectedRetryTime); + + let callCount = 0; + const next = async (): Promise => { + callCount++; + throw new Error('Test error'); + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(callCount).toBe(errorHandler.immediateRetryCount); + expect(transaction.outgoingMessages).toHaveLength(1); + + const delayedMessage = transaction.outgoingMessages[0]; + expect(delayedMessage.deliverAt).toEqual(expectedRetryTime); + expect(delayedMessage.headers.get(ErrorHandler.RetryCountHeader)).toBe('1'); + expect(delayedMessage.endpoint).toBe('TestBus'); + }); + + it('should increment retry count correctly when existing retry count header exists', async () => { + // Arrange + incomingMessage.headers.set(ErrorHandler.RetryCountHeader, '2'); + const expectedRetryTime = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes from now + errorHandler.setNextRetryTime(expectedRetryTime); + + const next = async (): Promise => { + throw new Error('Test error'); + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(transaction.outgoingMessages).toHaveLength(1); + + const delayedMessage = transaction.outgoingMessages[0]; + expect(delayedMessage.headers.get(ErrorHandler.RetryCountHeader)).toBe('3'); + expect(delayedMessage.deliverAt).toEqual(expectedRetryTime); + }); + + it('should send to dead letter queue when max delayed retries exceeded', async () => { + // Arrange + incomingMessage.headers.set( + ErrorHandler.RetryCountHeader, + errorHandler.delayedRetryCount.toString() + ); + + const testException = new Error('Final error'); + const next = async (): Promise => { + throw testException; + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(transaction.outgoingMessages).toHaveLength(1); + + const deadLetterMessage = transaction.outgoingMessages[0]; + expect(deadLetterMessage.endpoint).toBe('TestBus.Errors'); + expect(deadLetterMessage.headers.get(ErrorHandler.ErrorMessageHeader)).toBe('Final error'); + expect(deadLetterMessage.headers.get(ErrorHandler.ErrorStackTraceHeader)).toBeDefined(); + }); + + it('should use custom dead letter endpoint when specified', async () => { + // Arrange + errorHandler = new TestableErrorHandler(); + errorHandler.deadLetterEndpoint = 'CustomDeadLetter'; + + incomingMessage.headers.set( + ErrorHandler.RetryCountHeader, + errorHandler.delayedRetryCount.toString() + ); + + const next = async (): Promise => { + throw new Error('Final error'); + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(transaction.outgoingMessages).toHaveLength(1); + + const deadLetterMessage = transaction.outgoingMessages[0]; + expect(deadLetterMessage.endpoint).toBe('CustomDeadLetter'); + }); + + it('should clear outgoing messages on each retry', async () => { + // Arrange + let callCount = 0; + const next = async (): Promise => { + callCount++; + transaction.addOutgoingMessage('some message', 'some endpoint'); + throw new Error('Test error'); + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(callCount).toBe(errorHandler.immediateRetryCount); + // Should only have the delayed retry message, not the messages added in next() + expect(transaction.outgoingMessages).toHaveLength(1); + expect(transaction.outgoingMessages[0].headers.has(ErrorHandler.RetryCountHeader)).toBe(true); + }); + + it('should handle zero immediate retries with minimum of one', async () => { + // Arrange + errorHandler = new TestableErrorHandler(); + errorHandler.immediateRetryCount = 0; + const expectedRetryTime = new Date(Date.now() + 5 * 60 * 1000); + errorHandler.setNextRetryTime(expectedRetryTime); + + let callCount = 0; + const next = async (): Promise => { + callCount++; + throw new Error('Test error'); + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(callCount).toBe(1); // Should enforce minimum of 1 + expect(transaction.outgoingMessages).toHaveLength(1); + expect(transaction.outgoingMessages[0].headers.get(ErrorHandler.RetryCountHeader)).toBe('1'); + }); + + it('should handle zero delayed retries with minimum of one', async () => { + // Arrange + errorHandler = new TestableErrorHandler(); + errorHandler.delayedRetryCount = 0; + const expectedRetryTime = new Date(Date.now() + 5 * 60 * 1000); + errorHandler.setNextRetryTime(expectedRetryTime); + + const next = async (): Promise => { + throw new Error('Test error'); + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + // Even with delayedRetryCount = 0, Math.max(1, delayedRetryCount) makes it 1 + expect(transaction.outgoingMessages).toHaveLength(1); + expect(transaction.outgoingMessages[0].headers.get(ErrorHandler.RetryCountHeader)).toBe('1'); + expect(transaction.outgoingMessages[0].deliverAt).toEqual(expectedRetryTime); + }); + + it('should succeed after some immediate retries and stop retrying', async () => { + // Arrange + let callCount = 0; + const next = async (): Promise => { + callCount++; + if (callCount < 3) { // Fail first 2 attempts + throw new Error('Test error'); + } + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(callCount).toBe(3); + expect(transaction.outgoingMessages).toHaveLength(0); + }); + + it('should treat invalid retry count header as zero', async () => { + // Arrange + incomingMessage.headers.set(ErrorHandler.RetryCountHeader, 'invalid'); + const expectedRetryTime = new Date(Date.now() + 5 * 60 * 1000); + errorHandler.setNextRetryTime(expectedRetryTime); + + const next = async (): Promise => { + throw new Error('Test error'); + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(transaction.outgoingMessages).toHaveLength(1); + const delayedMessage = transaction.outgoingMessages[0]; + expect(delayedMessage.headers.get(ErrorHandler.RetryCountHeader)).toBe('1'); + }); + + it('should store error stack trace in header', async () => { + // Arrange + incomingMessage.headers.set( + ErrorHandler.RetryCountHeader, + errorHandler.delayedRetryCount.toString() + ); + + const errorWithStackTrace = new Error('Error with stack trace'); + + const next = async (): Promise => { + throw errorWithStackTrace; + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(transaction.outgoingMessages).toHaveLength(1); + const deadLetterMessage = transaction.outgoingMessages[0]; + expect(deadLetterMessage.headers.get(ErrorHandler.ErrorStackTraceHeader)).toBeDefined(); + expect(deadLetterMessage.headers.get(ErrorHandler.ErrorStackTraceHeader)).not.toBe(''); + }); + + it('should use empty string for null stack trace', async () => { + // Arrange + incomingMessage.headers.set( + ErrorHandler.RetryCountHeader, + errorHandler.delayedRetryCount.toString() + ); + + // Create an error with null stack trace using custom error + const errorWithoutStackTrace = new ErrorWithNullStackTrace('Error without stack trace'); + + const next = async (): Promise => { + throw errorWithoutStackTrace; + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(transaction.outgoingMessages).toHaveLength(1); + const deadLetterMessage = transaction.outgoingMessages[0]; + expect(deadLetterMessage.headers.get(ErrorHandler.ErrorStackTraceHeader)).toBe(''); + }); + + it('should handle non-Error exceptions', async () => { + // Arrange + incomingMessage.headers.set( + ErrorHandler.RetryCountHeader, + errorHandler.delayedRetryCount.toString() + ); + + const next = async (): Promise => { + throw 'String error'; // eslint-disable-line @typescript-eslint/no-throw-literal + }; + + // Act + await errorHandler.retrier(transaction, next); + + // Assert + expect(transaction.outgoingMessages).toHaveLength(1); + const deadLetterMessage = transaction.outgoingMessages[0]; + expect(deadLetterMessage.headers.get(ErrorHandler.ErrorMessageHeader)).toBe('String error'); + expect(deadLetterMessage.headers.get(ErrorHandler.ErrorStackTraceHeader)).toBe(''); + }); + }); + + describe('getNextRetryTime method', () => { + it('should calculate retry time correctly with default delay', () => { + // Arrange + const handler = new ErrorHandler(); + handler.delay = 60; + const beforeTime = new Date(); + + // Act + const result1 = handler.getNextRetryTime(1); + const result2 = handler.getNextRetryTime(2); + const result3 = handler.getNextRetryTime(3); + + const afterTime = new Date(); + + // Assert + expect(result1.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime() + 60 * 1000); + expect(result1.getTime()).toBeLessThanOrEqual(afterTime.getTime() + 60 * 1000); + + expect(result2.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime() + 120 * 1000); + expect(result2.getTime()).toBeLessThanOrEqual(afterTime.getTime() + 120 * 1000); + + expect(result3.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime() + 180 * 1000); + expect(result3.getTime()).toBeLessThanOrEqual(afterTime.getTime() + 180 * 1000); + }); + }); + + describe('static constants', () => { + it('should have correct error header constants', () => { + expect(ErrorHandler.ErrorMessageHeader).toBe('X-Error-Message'); + expect(ErrorHandler.ErrorStackTraceHeader).toBe('X-Error-Stack-Trace'); + expect(ErrorHandler.RetryCountHeader).toBe('X-Retry-Count'); + }); + }); + + describe('default configuration', () => { + it('should have correct default values', () => { + const handler = new ErrorHandler(); + expect(handler.delay).toBe(30); + expect(handler.delayedRetryCount).toBe(3); + expect(handler.immediateRetryCount).toBe(3); + expect(handler.deadLetterEndpoint).toBeUndefined(); + }); + }); +}); + +// Helper class to override getNextRetryTime for testing +class TestableErrorHandler extends ErrorHandler { + private nextRetryTime?: Date; + + public setNextRetryTime(retryTime: Date): void { + this.nextRetryTime = retryTime; + } + + public override getNextRetryTime(attempt: number): Date { + return this.nextRetryTime ?? super.getNextRetryTime(attempt); + } +} + +// Custom error that returns undefined for stack trace +class ErrorWithNullStackTrace extends Error { + constructor(message: string) { + super(message); + // Clear the stack trace by deleting the property + delete (this as any).stack; + } +} + +// Test implementation of IPolyBus for testing purposes +class TestBus implements IPolyBus { + public transport: ITransport; + public incomingHandlers: IncomingHandler[] = []; + public outgoingHandlers: OutgoingHandler[] = []; + public messages: Messages = new Messages(); + public properties: Map = new Map(); + + constructor(public name: string) { + this.transport = new TestTransport(); + } + + public async createTransaction(message?: IncomingMessage): Promise { + const transaction: Transaction = message == null + ? new OutgoingTransaction(this) + : new IncomingTransaction(this, message); + return transaction; + } + + public async send(_transaction: Transaction): Promise { + // Mock implementation + } + + public async start(): Promise { + // Mock implementation + } + + public async stop(): Promise { + // Mock implementation + } +} + +// Simple test transport implementation +class TestTransport implements ITransport { + public supportsCommandMessages = true; + public supportsDelayedMessages = true; + public supportsSubscriptions = false; + + public async send(_transaction: Transaction): Promise { + // Mock implementation + } + + public async subscribe(): Promise { + // Mock implementation + } + + public async start(): Promise { + // Mock implementation + } + + public async stop(): Promise { + // Mock implementation + } +} \ No newline at end of file diff --git a/src/typescript/src/transport/transaction/message/handlers/error/error-handlers.ts b/src/typescript/src/transport/transaction/message/handlers/error/error-handlers.ts new file mode 100644 index 0000000..587e820 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/handlers/error/error-handlers.ts @@ -0,0 +1,96 @@ +import { IncomingTransaction } from '../../../incoming-transaction'; + +/** + * Handles error scenarios for message processing, including retries and dead letter queues. + */ +export class ErrorHandler { + public static readonly ErrorMessageHeader = 'X-Error-Message'; + public static readonly ErrorStackTraceHeader = 'X-Error-Stack-Trace'; + public static readonly RetryCountHeader = 'X-Retry-Count'; + + /** + * The delay in seconds between retry attempts. + * @default 30 + */ + public delay: number = 30; + + /** + * The number of delayed retry attempts. + * @default 3 + */ + public delayedRetryCount: number = 3; + + /** + * The number of immediate retry attempts. + * @default 3 + */ + public immediateRetryCount: number = 3; + + /** + * The endpoint to send messages to when all retries are exhausted. + * If not specified, defaults to {busName}.Errors + */ + public deadLetterEndpoint?: string; + + /** + * Retry handler that implements immediate and delayed retry logic. + * @param transaction The incoming transaction to process. + * @param next The next function in the pipeline to execute. + */ + public async retrier( + transaction: IncomingTransaction, + next: () => Promise + ): Promise { + const headerValue = transaction.incomingMessage.headers.get(ErrorHandler.RetryCountHeader); + const delayedAttempt = headerValue ? parseInt(headerValue, 10) || 0 : 0; + const delayedRetryCount = Math.max(1, this.delayedRetryCount); + const immediateRetryCount = Math.max(1, this.immediateRetryCount); + + for (let immediateAttempt = 0; immediateAttempt < immediateRetryCount; immediateAttempt++) { + try { + await next(); + break; + } catch (error) { + transaction.outgoingMessages.length = 0; // Clear outgoing messages + + if (immediateAttempt < immediateRetryCount - 1) { + continue; + } + + if (delayedAttempt < delayedRetryCount) { + // Re-queue the message with a delay + const nextDelayedAttempt = delayedAttempt + 1; + + const delayedMessage = transaction.addOutgoingMessage( + transaction.incomingMessage.message, + transaction.bus.name + ); + delayedMessage.deliverAt = this.getNextRetryTime(nextDelayedAttempt); + delayedMessage.headers.set(ErrorHandler.RetryCountHeader, nextDelayedAttempt.toString()); + + continue; + } + + const deadLetterEndpoint = this.deadLetterEndpoint ?? `${transaction.bus.name}.Errors`; + const deadLetterMessage = transaction.addOutgoingMessage( + transaction.incomingMessage.message, + deadLetterEndpoint + ); + deadLetterMessage.headers.set(ErrorHandler.ErrorMessageHeader, error instanceof Error ? error.message : String(error)); + deadLetterMessage.headers.set( + ErrorHandler.ErrorStackTraceHeader, + error instanceof Error ? error.stack ?? '' : '' + ); + } + } + } + + /** + * Calculates the next retry time based on the attempt number. + * @param attempt The current attempt number. + * @returns The Date when the next retry should be attempted. + */ + public getNextRetryTime(attempt: number): Date { + return new Date(Date.now() + attempt * this.delay * 1000); + } +} diff --git a/src/typescript/src/transport/transaction/message/handlers/incoming-handler.ts b/src/typescript/src/transport/transaction/message/handlers/incoming-handler.ts new file mode 100644 index 0000000..e493954 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/handlers/incoming-handler.ts @@ -0,0 +1,6 @@ +import { IncomingTransaction } from '../../incoming-transaction'; + +/** + * A method for handling incoming messages from the transport. + */ +export type IncomingHandler = (transaction: IncomingTransaction, next: () => Promise) => Promise; diff --git a/src/typescript/src/transport/transaction/message/handlers/outgoing-handler.ts b/src/typescript/src/transport/transaction/message/handlers/outgoing-handler.ts new file mode 100644 index 0000000..f691a15 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/handlers/outgoing-handler.ts @@ -0,0 +1,6 @@ +import { OutgoingTransaction } from '../../outgoing-transaction'; + +/** + * A method for handling outgoing messages to the transport. + */ +export type OutgoingHandler = (transaction: OutgoingTransaction, next: () => Promise) => Promise; diff --git a/src/typescript/src/transport/transaction/message/handlers/serializers/__tests__/json-handlers.test.ts b/src/typescript/src/transport/transaction/message/handlers/serializers/__tests__/json-handlers.test.ts new file mode 100644 index 0000000..dc13e4a --- /dev/null +++ b/src/typescript/src/transport/transaction/message/handlers/serializers/__tests__/json-handlers.test.ts @@ -0,0 +1,493 @@ +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; +import { Headers } from '../../../../../../headers'; +import { IncomingMessage } from '../../../incoming-message'; +import { IncomingTransaction } from '../../../../incoming-transaction'; +import { IPolyBus } from '../../../../../../i-poly-bus'; +import { JsonHandlers } from '../json-handlers'; +import { messageInfo } from '../../../message-info'; +import { Messages } from '../../../messages'; +import { MessageType } from '../../../message-info'; +import { OutgoingTransaction } from '../../../../outgoing-transaction'; + +/** + * Jest tests for JsonHandlers serialization and deserialization functionality. + * + * These tests are based on the C# NUnit tests for the JsonHandlers class, + * adapted for TypeScript and Jest testing framework. The tests cover: + * + * - Deserializer functionality with valid/invalid type headers + * - Custom JSON reviver functions + * - Error handling for missing/invalid types + * - Serializer functionality with message type headers + * - Custom JSON replacer functions and content types + * - Multiple message handling + * - Configuration properties and defaults + */ + +/** + * Test class for JsonHandlers serialization and deserialization + */ +describe('JsonHandlers', () => { + let jsonHandlers: JsonHandlers; + let mockBus: jest.Mocked; + let messages: Messages; + + beforeEach(() => { + jsonHandlers = new JsonHandlers(); + messages = new Messages(); + + mockBus = { + transport: {} as any, + incomingHandlers: [], + outgoingHandlers: [], + messages: messages, + name: 'MockBus', + createTransaction: jest.fn(), + send: jest.fn(), + start: jest.fn(), + stop: jest.fn() + } as any; + }); + + describe('Deserializer Tests', () => { + it('should deserialize message with valid type header', async () => { + // Arrange + const testMessage = { id: 1, name: 'Test' }; + const serializedBody = JSON.stringify(testMessage); + + @messageInfo(MessageType.Command, 'test-service', 'TestMessage', 1, 0, 0) + class TestMessage { + public id: number = 0; + public name: string = ''; + } + + messages.add(TestMessage); + const header = 'endpoint=test-service, type=Command, name=TestMessage, version=1.0.0'; + + const incomingMessage = new IncomingMessage(mockBus, serializedBody); + incomingMessage.headers.set(Headers.MessageType, header); + + const transaction = new MockIncomingTransaction(mockBus, incomingMessage); + + let nextCalled = false; + const next = async () => { nextCalled = true; }; + + // Act + await jsonHandlers.deserializer(transaction, next); + + // Assert + expect(nextCalled).toBe(true); + expect(incomingMessage.message).not.toBeNull(); + expect(incomingMessage.message).toEqual(testMessage); + }); + + it('should deserialize message with custom JSON reviver', async () => { + // Arrange + const jsonHandlers = new JsonHandlers(); + jsonHandlers.jsonReviver = (key: string, value: any) => { + if (key === 'name' && typeof value === 'string') { + return value.toUpperCase(); + } + return value; + }; + + const testMessage = { id: 2, name: 'test' }; + const serializedBody = JSON.stringify(testMessage); + + @messageInfo(MessageType.Command, 'test-service', 'TestMessage', 1, 0, 0) + class TestMessage { + public id: number = 0; + public name: string = ''; + } + + messages.add(TestMessage); + const header = 'endpoint=test-service, type=Command, name=TestMessage, version=1.0.0'; + + const incomingMessage = new IncomingMessage(mockBus, serializedBody); + incomingMessage.headers.set(Headers.MessageType, header); + + const transaction = new MockIncomingTransaction(mockBus, incomingMessage); + + let nextCalled = false; + const next = async () => { nextCalled = true; }; + + // Act + await jsonHandlers.deserializer(transaction, next); + + // Assert + expect(nextCalled).toBe(true); + expect(incomingMessage.message).toEqual({ id: 2, name: 'TEST' }); + }); + + it('should parse as generic object when type is unknown and throwOnMissingType is false', async () => { + // Arrange + const jsonHandlers = new JsonHandlers(); + jsonHandlers.throwOnMissingType = false; + + const testObject = { id: 3, name: 'Unknown' }; + const serializedBody = JSON.stringify(testObject); + const header = 'endpoint=test-service, type=Command, name=UnknownMessage, version=1.0.0'; + + const incomingMessage = new IncomingMessage(mockBus, serializedBody); + incomingMessage.headers.set(Headers.MessageType, header); + + const transaction = new MockIncomingTransaction(mockBus, incomingMessage); + + let nextCalled = false; + const next = async () => { nextCalled = true; }; + + // Act + await jsonHandlers.deserializer(transaction, next); + + // Assert + expect(nextCalled).toBe(true); + expect(incomingMessage.message).not.toBeNull(); + expect(incomingMessage.message).toEqual(testObject); + }); + + it('should throw exception when type is unknown and throwOnMissingType is true', async () => { + // Arrange + const jsonHandlers = new JsonHandlers(); + jsonHandlers.throwOnMissingType = true; + + const testObject = { id: 4, name: 'Error' }; + const serializedBody = JSON.stringify(testObject); + const header = 'endpoint=test-service, type=Command, name=UnknownMessage, version=1.0.0'; + + const incomingMessage = new IncomingMessage(mockBus, serializedBody); + incomingMessage.headers.set(Headers.MessageType, header); + + const transaction = new MockIncomingTransaction(mockBus, incomingMessage); + + const next = async () => {}; + + // Act & Assert + await expect(jsonHandlers.deserializer(transaction, next)) + .rejects.toThrow('The type header is missing, invalid, or if the type cannot be found.'); + }); + + it('should throw exception when type header is missing and throwOnMissingType is true', async () => { + // Arrange + const jsonHandlers = new JsonHandlers(); + jsonHandlers.throwOnMissingType = true; + + const incomingMessage = new IncomingMessage(mockBus, '{}'); + const transaction = new MockIncomingTransaction(mockBus, incomingMessage); + + const next = async () => {}; + + // Act & Assert + await expect(jsonHandlers.deserializer(transaction, next)) + .rejects.toThrow('The type header is missing, invalid, or if the type cannot be found.'); + }); + + it('should throw error when JSON is invalid', async () => { + // Arrange + @messageInfo(MessageType.Command, 'test-service', 'TestMessage', 1, 0, 0) + class TestMessage { + public id: number = 0; + public name: string = ''; + } + + messages.add(TestMessage); + const header = 'endpoint=test-service, type=Command, name=TestMessage, version=1.0.0'; + + const incomingMessage = new IncomingMessage(mockBus, 'invalid json'); + incomingMessage.headers.set(Headers.MessageType, header); + + const transaction = new MockIncomingTransaction(mockBus, incomingMessage); + + const next = async () => {}; + + // Act & Assert + await expect(jsonHandlers.deserializer(transaction, next)) + .rejects.toThrow('Failed to parse JSON message body:'); + }); + }); + + describe('Serializer Tests', () => { + it('should serialize message and set headers', async () => { + // Arrange + @messageInfo(MessageType.Command, 'test-service', 'TestMessage', 1, 0, 0) + class TestMessage { + public id: number; + public name: string; + + constructor(id: number, name: string) { + this.id = id; + this.name = name; + } + } + + const testMessage = new TestMessage(5, 'Serialize'); + messages.add(TestMessage); + + const mockTransaction = new MockOutgoingTransaction(mockBus); + const outgoingMessage = mockTransaction.addOutgoingMessage(testMessage); + + let nextCalled = false; + const next = async () => { nextCalled = true; }; + + // Act + await jsonHandlers.serializer(mockTransaction, next); + + // Assert + expect(nextCalled).toBe(true); + expect(outgoingMessage.body).not.toBeNull(); + + const deserializedMessage = JSON.parse(outgoingMessage.body); + expect(deserializedMessage.id).toBe(5); + expect(deserializedMessage.name).toBe('Serialize'); + + expect(outgoingMessage.headers.get(Headers.ContentType)).toBe('application/json'); + expect(outgoingMessage.headers.get(Headers.MessageType)).toBe('endpoint=test-service, type=command, name=TestMessage, version=1.0.0'); + }); + + it('should use custom content type when specified', async () => { + // Arrange + const customContentType = 'application/custom-json'; + const jsonHandlers = new JsonHandlers(); + jsonHandlers.contentType = customContentType; + jsonHandlers.throwOnInvalidType = false; + + @messageInfo(MessageType.Command, 'test-service', 'TestMessage', 1, 0, 0) + class TestMessage { + public id: number; + public name: string; + + constructor(id: number, name: string) { + this.id = id; + this.name = name; + } + } + + const testMessage = new TestMessage(6, 'Custom'); + messages.add(TestMessage); + + const mockTransaction = new MockOutgoingTransaction(mockBus); + const outgoingMessage = mockTransaction.addOutgoingMessage(testMessage); + + let nextCalled = false; + const next = async () => { nextCalled = true; }; + + // Act + await jsonHandlers.serializer(mockTransaction, next); + + // Assert + expect(nextCalled).toBe(true); + expect(outgoingMessage.headers.get(Headers.ContentType)).toBe(customContentType); + }); + + it('should serialize with custom JSON replacer', async () => { + // Arrange + const jsonHandlers = new JsonHandlers(); + jsonHandlers.jsonReplacer = (key: string, value: any) => { + if (key === 'name' && typeof value === 'string') { + return value.toLowerCase(); + } + return value; + }; + jsonHandlers.throwOnInvalidType = false; + + @messageInfo(MessageType.Command, 'test-service', 'TestMessage', 1, 0, 0) + class TestMessage { + public id: number; + public name: string; + + constructor(id: number, name: string) { + this.id = id; + this.name = name; + } + } + + const testMessage = new TestMessage(7, 'OPTIONS'); + messages.add(TestMessage); + + const mockTransaction = new MockOutgoingTransaction(mockBus); + const outgoingMessage = mockTransaction.addOutgoingMessage(testMessage); + + let nextCalled = false; + const next = async () => { nextCalled = true; }; + + // Act + await jsonHandlers.serializer(mockTransaction, next); + + // Assert + expect(nextCalled).toBe(true); + expect(outgoingMessage.body).toContain('"name":"options"'); + }); + + it('should skip header setting when type is unknown and throwOnInvalidType is false', async () => { + // Arrange + const jsonHandlers = new JsonHandlers(); + jsonHandlers.throwOnInvalidType = false; + + class UnknownMessage { + public data: string; + + constructor(data: string) { + this.data = data; + } + } + + const testMessage = new UnknownMessage('test'); + + const mockTransaction = new MockOutgoingTransaction(mockBus); + const outgoingMessage = mockTransaction.addOutgoingMessage(testMessage, 'unknown-endpoint'); + + let nextCalled = false; + const next = async () => { nextCalled = true; }; + + // Act + await jsonHandlers.serializer(mockTransaction, next); + + // Assert + expect(nextCalled).toBe(true); + expect(outgoingMessage.body).not.toBeNull(); + expect(outgoingMessage.headers.get(Headers.ContentType)).toBe('application/json'); + expect(outgoingMessage.headers.has(Headers.MessageType)).toBe(false); + }); + + it('should throw exception when type is unknown and throwOnInvalidType is true', async () => { + // Arrange + const jsonHandlers = new JsonHandlers(); + jsonHandlers.throwOnInvalidType = true; + + class UnknownMessage { + public data: string; + + constructor(data: string) { + this.data = data; + } + } + + const testMessage = new UnknownMessage('error'); + + const mockTransaction = new MockOutgoingTransaction(mockBus); + mockTransaction.addOutgoingMessage(testMessage, 'unknown-endpoint'); + + const next = async () => {}; + + // Act & Assert + await expect(jsonHandlers.serializer(mockTransaction, next)) + .rejects.toThrow('The header has an invalid type.'); + }); + + it('should serialize multiple messages', async () => { + // Arrange + @messageInfo(MessageType.Command, 'test-service', 'TestMessage', 1, 0, 0) + class TestMessage { + public id: number; + public name: string; + + constructor(id: number, name: string) { + this.id = id; + this.name = name; + } + } + + const testMessage1 = new TestMessage(8, 'First'); + const testMessage2 = new TestMessage(9, 'Second'); + + messages.add(TestMessage); + + const mockTransaction = new MockOutgoingTransaction(mockBus); + const outgoingMessage1 = mockTransaction.addOutgoingMessage(testMessage1); + const outgoingMessage2 = mockTransaction.addOutgoingMessage(testMessage2); + + let nextCalled = false; + const next = async () => { nextCalled = true; }; + + // Act + await jsonHandlers.serializer(mockTransaction, next); + + // Assert + expect(nextCalled).toBe(true); + expect(outgoingMessage1.body).not.toBeNull(); + expect(outgoingMessage2.body).not.toBeNull(); + + const deserializedMessage1 = JSON.parse(outgoingMessage1.body); + const deserializedMessage2 = JSON.parse(outgoingMessage2.body); + + expect(deserializedMessage1.id).toBe(8); + expect(deserializedMessage1.name).toBe('First'); + expect(deserializedMessage2.id).toBe(9); + expect(deserializedMessage2.name).toBe('Second'); + }); + + it('should call next when no outgoing messages exist', async () => { + // Arrange + const mockTransaction = new MockOutgoingTransaction(mockBus); + + let nextCalled = false; + const next = async () => { nextCalled = true; }; + + // Act + await jsonHandlers.serializer(mockTransaction, next); + + // Assert + expect(nextCalled).toBe(true); + }); + }); + + describe('Configuration Properties', () => { + it('should have default content type as application/json', () => { + const handlers = new JsonHandlers(); + expect(handlers.contentType).toBe('application/json'); + }); + + it('should have default throwOnMissingType as true', () => { + const handlers = new JsonHandlers(); + expect(handlers.throwOnMissingType).toBe(true); + }); + + it('should have default throwOnInvalidType as true', () => { + const handlers = new JsonHandlers(); + expect(handlers.throwOnInvalidType).toBe(true); + }); + + it('should allow custom configuration', () => { + const handlers = new JsonHandlers(); + handlers.contentType = 'custom/type'; + handlers.throwOnMissingType = false; + handlers.throwOnInvalidType = false; + handlers.jsonReplacer = (_key, value) => value; + handlers.jsonReviver = (_key, value) => value; + + expect(handlers.contentType).toBe('custom/type'); + expect(handlers.throwOnMissingType).toBe(false); + expect(handlers.throwOnInvalidType).toBe(false); + expect(handlers.jsonReplacer).toBeDefined(); + expect(handlers.jsonReviver).toBeDefined(); + }); + }); +}); + +// Mock classes to support the tests +class MockIncomingTransaction extends IncomingTransaction { + constructor(bus: IPolyBus, incomingMessage: IncomingMessage) { + super(bus, incomingMessage); + } + + public override async abort(): Promise { + // Mock implementation + } + + public override async commit(): Promise { + // Mock implementation + } +} + +class MockOutgoingTransaction extends OutgoingTransaction { + constructor(bus: IPolyBus) { + super(bus); + } + + public override async abort(): Promise { + // Mock implementation + } + + public override async commit(): Promise { + // Mock implementation + } +} diff --git a/src/typescript/src/transport/transaction/message/handlers/serializers/json-handlers.ts b/src/typescript/src/transport/transaction/message/handlers/serializers/json-handlers.ts new file mode 100644 index 0000000..64c39b4 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/handlers/serializers/json-handlers.ts @@ -0,0 +1,99 @@ +import { Headers } from '../../../../../headers'; +import { IncomingTransaction } from '../../../incoming-transaction'; +import { OutgoingTransaction } from '../../../outgoing-transaction'; + +/** + * JSON serialization and deserialization handlers for PolyBus messages. + * Uses standard JSON.stringify and JSON.parse methods for processing. + */ +export class JsonHandlers { + /** + * Custom JSON replacer function to use during serialization. + * Equivalent to JsonSerializerOptions in C#. + */ + public jsonReplacer?: (key: string, value: any) => any; + + /** + * Custom JSON reviver function to use during deserialization. + * Equivalent to JsonSerializerOptions in C#. + */ + public jsonReviver?: (key: string, value: any) => any; + + /** + * The content type to set on outgoing messages. + */ + public contentType: string = 'application/json'; + + /** + * If the type header is missing, invalid, or if the type cannot be found, throw an exception. + */ + public throwOnMissingType: boolean = true; + + /** + * If the message type is not in the list of known messages, throw an exception. + */ + public throwOnInvalidType: boolean = true; + + /** + * Deserializes incoming JSON messages. + * @param transaction The incoming transaction containing the message to deserialize. + * @param next The next handler in the pipeline. + */ + public async deserializer(transaction: IncomingTransaction, next: () => Promise): Promise { + const message = transaction.incomingMessage; + + // Try to get the message type from headers + const header = message.headers.get(Headers.MessageType); + const type = header ? message.bus.messages.getTypeByHeader(header) : null; + + if (type === null && this.throwOnMissingType) { + throw new Error('The type header is missing, invalid, or if the type cannot be found.'); + } + + // If we have a known type, we could potentially use it for validation + // But since TypeScript doesn't have runtime type information like C#, + // we'll just parse the JSON and trust the type system + try { + message.message = JSON.parse(message.body, this.jsonReviver); + + // If we found a type, we could set the messageType for consistency + if (type !== null) { + message.messageType = type; + } + } catch (error) { + throw new Error(`Failed to parse JSON message body: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + + await next(); + } + + /** + * Serializes outgoing messages to JSON. + * @param transaction The outgoing transaction containing messages to serialize. + * @param next The next handler in the pipeline. + */ + public async serializer(transaction: OutgoingTransaction, next: () => Promise): Promise { + for (const message of transaction.outgoingMessages) { + // Serialize the message to JSON + try { + message.body = JSON.stringify(message.message, this.jsonReplacer); + } catch (error) { + throw new Error(`Failed to serialize message to JSON: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + + // Set content type + message.headers.set(Headers.ContentType, this.contentType); + + // Try to get and set the message type header + const header = message.bus.messages.getHeader(message.messageType); + + if (header !== null) { + message.headers.set(Headers.MessageType, header); + } else if (this.throwOnInvalidType) { + throw new Error('The header has an invalid type.'); + } + } + + await next(); + } +} diff --git a/src/typescript/src/transport/transaction/message/incoming-message.ts b/src/typescript/src/transport/transaction/message/incoming-message.ts new file mode 100644 index 0000000..024a881 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/incoming-message.ts @@ -0,0 +1,64 @@ +import { IPolyBus } from '../../../i-poly-bus'; +import { Message } from './message'; + +/** + * Represents an incoming message in the transport layer. + * Contains the message body, deserialized message object, and message type information. + */ +export class IncomingMessage extends Message { + private _messageType: any; + private _body: string; + private _message: any; + + /** + * Creates a new IncomingMessage instance. + * @param bus The bus instance associated with the message. + * @param body The message body contents. + * @param message The deserialized message object (optional, defaults to body). + * @param messageType The type of the message (optional, defaults to string). + */ + constructor(bus: IPolyBus, body: string, message?: any, messageType?: any) { + super(bus); + + if (!body) { + throw new Error('Body parameter cannot be null or undefined'); + } + + this._body = body; + this._message = message ?? body; + this._messageType = messageType ?? String; + } + + /** + * The default is string, but can be changed based on deserialization. + */ + public get messageType(): any { + return this._messageType; + } + + public set messageType(value: any) { + this._messageType = value; + } + + /** + * The message body contents. + */ + public get body(): string { + return this._body; + } + + public set body(value: string) { + this._body = value; + } + + /** + * The deserialized message object, otherwise the same value as Body. + */ + public get message(): any { + return this._message; + } + + public set message(value: any) { + this._message = value; + } +} diff --git a/src/typescript/src/transport/transaction/message/message-info.ts b/src/typescript/src/transport/transaction/message/message-info.ts new file mode 100644 index 0000000..dea8b84 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/message-info.ts @@ -0,0 +1,133 @@ +import { MessageType } from './message-type'; + +/** + * Interface to store message metadata on a class constructor + */ +export interface MessageInfoMetadata { + type: MessageType; + endpoint: string; + name: string; + major: number; + minor: number; + patch: number; +} + +/** + * This decorates a message class with metadata about the message. + * This is used to identify the message type and version so that it can be routed and deserialized appropriately. + */ +export class MessageInfo { + private static readonly HEADER_PATTERN = /^endpoint\s*=\s*(?[^,\s]+),\s*type\s*=\s*(?[^,\s]+),\s*name\s*=\s*(?[^,\s]+),\s*version\s*=\s*(?\d+)\.(?\d+)\.(?\d+)/i; + private static readonly METADATA_KEY = Symbol('messageInfo'); + + constructor( + public readonly type: MessageType, + public readonly endpoint: string, + public readonly name: string, + public readonly major: number, + public readonly minor: number, + public readonly patch: number + ) {} + + /** + * Parses a message attribute from a message header string. + * @param header The header string to parse + * @returns If the header is valid, returns a MessageInfo instance; otherwise, returns null. + */ + public static getAttributeFromHeader(header: string): MessageInfo | null { + const match = this.HEADER_PATTERN.exec(header); + + if (!match || !match.groups) { + return null; + } + + const endpoint = match.groups.endpoint; + const name = match.groups.name; + const typeStr = match.groups.type.toLowerCase(); + const major = parseInt(match.groups.major, 10); + const minor = parseInt(match.groups.minor, 10); + const patch = parseInt(match.groups.patch, 10); + + // Parse MessageType enum + let type: MessageType; + if (typeStr === 'command') { + type = MessageType.Command; + } else if (typeStr === 'event') { + type = MessageType.Event; + } else { + return null; // Invalid message type + } + + return new MessageInfo(type, endpoint, name, major, minor, patch); + } + + /** + * Gets the MessageInfo metadata from a class constructor + * @param target The class constructor to get metadata from + * @returns The MessageInfo instance if found, null otherwise + */ + public static getMetadata(target: any): MessageInfo | null { + return Reflect.getMetadata?.(this.METADATA_KEY, target) || target[this.METADATA_KEY] || null; + } + + /** + * Compares two message attributes for equality. + * The patch and minor versions are not considered for equality. + * @param other The other MessageInfo to compare with + * @returns True if equal, false otherwise + */ + public equals(other: MessageInfo | null): boolean { + return other !== null + && this.type === other.type + && this.endpoint === other.endpoint + && this.name === other.name + && this.major === other.major; + } + + /** + * Serializes the message attribute to a string format suitable for message headers. + * @param includeVersion Whether to include version information + * @returns The serialized string + */ + public toString(includeVersion: boolean = true): string { + const typeStr = this.type === MessageType.Command ? 'command' : 'event'; + const base = `endpoint=${this.endpoint}, type=${typeStr}, name=${this.name}`; + return includeVersion ? `${base}, version=${this.major}.${this.minor}.${this.patch}` : base; + } +} + +/** + * Class decorator factory for attaching MessageInfo metadata to message classes. + * @param type If the message is a command or event + * @param endpoint The endpoint that publishes the event message or the endpoint that handles the command + * @param name The unique name for the message for the given endpoint + * @param major The major version of the message schema + * @param minor The minor version of the message schema + * @param patch The patch version of the message schema + * @returns Class decorator function + */ +export function messageInfo( + type: MessageType, + endpoint: string, + name: string, + major: number, + minor: number, + patch: number +) { + return function any>(target: T): T { + const metadata = new MessageInfo(type, endpoint, name, major, minor, patch); + + // Use reflect-metadata if available, otherwise store directly on the constructor + if (typeof Reflect !== 'undefined' && Reflect.defineMetadata) { + Reflect.defineMetadata(MessageInfo['METADATA_KEY'], metadata, target); + } else { + // Fallback: store directly on the constructor + (target as any)[MessageInfo['METADATA_KEY']] = metadata; + } + + return target; + }; +} + +// Re-export for convenience +export { MessageType }; diff --git a/src/typescript/src/transport/transaction/message/message-type.ts b/src/typescript/src/transport/transaction/message/message-type.ts new file mode 100644 index 0000000..d976a0a --- /dev/null +++ b/src/typescript/src/transport/transaction/message/message-type.ts @@ -0,0 +1,16 @@ +/** + * Represents the type of message. + */ +export enum MessageType { + /** + * Command message type. + * Commands are messages that are sent to and processed by a single endpoint. + */ + Command, + + /** + * Event message type. + * Events are messages that can be processed by multiple endpoints and sent from a single endpoint. + */ + Event +} diff --git a/src/typescript/src/transport/transaction/message/message.ts b/src/typescript/src/transport/transaction/message/message.ts new file mode 100644 index 0000000..1ff1a54 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/message.ts @@ -0,0 +1,47 @@ +import { IPolyBus } from '../../../i-poly-bus'; + +/** + * Message class that represents a message in the transport layer. + * Contains state dictionary, headers, and reference to the bus instance. + */ +export class Message { + private readonly _state: Map = new Map(); + private _headers: Map = new Map(); + private readonly _bus: IPolyBus; + + /** + * Creates a new Message instance. + * @param bus The bus instance associated with the message. + */ + constructor(bus: IPolyBus) { + if (!bus) { + throw new Error('Bus parameter cannot be null or undefined'); + } + this._bus = bus; + } + + /** + * State dictionary that can be used to store arbitrary data associated with the message. + */ + public get state(): Map { + return this._state; + } + + /** + * Message headers from the transport. + */ + public get headers(): Map { + return this._headers; + } + + public set headers(value: Map) { + this._headers = value || new Map(); + } + + /** + * The bus instance associated with the message. + */ + public get bus(): IPolyBus { + return this._bus; + } +} diff --git a/src/typescript/src/transport/transaction/message/messages.ts b/src/typescript/src/transport/transaction/message/messages.ts new file mode 100644 index 0000000..bdb3d48 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/messages.ts @@ -0,0 +1,105 @@ +import { MessageInfo } from './message-info'; + +/** + * Interface for storing message type and metadata information + */ +interface MessageEntry { + attribute: MessageInfo; + header: string; +} + +/** + * A collection of message types and their associated message headers. + */ +export class Messages { + private readonly _map = new Map(); + private readonly _types = new Map(); + + /** + * Gets the message attribute associated with the specified type. + */ + public getMessageInfo(type: Function): MessageInfo | null { + const entry = this._types.get(type); + return entry ? entry.attribute : null; + } + + /** + * Attempts to get the message type (constructor function) associated with the specified header. + * @param header The message header string to look up + * @returns If found, returns the message constructor function; otherwise, returns null. + */ + public getTypeByHeader(header: string): Function | null { + const attribute = MessageInfo.getAttributeFromHeader(header); + if (!attribute) { + return null; + } + + // Check if we already have this header cached + if (this._map.has(header)) { + return this._map.get(header) || null; + } + + // Find the type that matches this attribute + for (const [type, entry] of this._types.entries()) { + if (entry.attribute.equals(attribute)) { + this._map.set(header, type); + return type; + } + } + + // Not found, cache null result + this._map.set(header, null); + return null; + } + + /** + * Attempts to get the message header associated with the specified type. + * @param type The message constructor function to look up + * @returns If found, returns the message header; otherwise, returns null. + */ + public getHeader(type: Function): string | null { + const entry = this._types.get(type); + return entry ? entry.header : null; + } + + /** + * Adds a message type to the collection. + * The message type must have MessageInfo metadata defined via the @messageInfo decorator. + * @param messageType The message constructor function to add + * @returns The MessageInfo associated with the message type. + * @throws Error if the type does not have MessageInfo metadata + * @throws Error if the type has already been added + */ + public add(messageType: Function): MessageInfo { + if (this._types.has(messageType)) { + throw new Error(`Type ${messageType.name} has already been added to the Messages collection.`); + } + + const attribute = MessageInfo.getMetadata(messageType); + if (!attribute) { + throw new Error(`Type ${messageType.name} does not have MessageInfo metadata. Make sure to use the @messageInfo decorator.`); + } + + const header = attribute.toString(true); + const entry: MessageEntry = { attribute, header }; + + this._types.set(messageType, entry); + this._map.set(header, messageType); + + return attribute; + } + + /** + * Attempts to get the message type associated with the specified MessageInfo. + * @param messageInfo The MessageInfo to look up + * @returns If found, returns the message constructor function; otherwise, returns null. + */ + public getTypeByMessageInfo(messageInfo: MessageInfo): Function | null { + for (const [type, entry] of this._types.entries()) { + if (entry.attribute.equals(messageInfo)) { + return type; + } + } + return null; + } +} diff --git a/src/typescript/src/transport/transaction/message/outgoing-message.ts b/src/typescript/src/transport/transaction/message/outgoing-message.ts new file mode 100644 index 0000000..ea0d461 --- /dev/null +++ b/src/typescript/src/transport/transaction/message/outgoing-message.ts @@ -0,0 +1,88 @@ +import { IPolyBus } from '../../../i-poly-bus'; +import { Message } from './message'; + +/** + * Represents an outgoing message in the transport layer. + * Contains the message object, its type, and serialized body content. + */ +export class OutgoingMessage extends Message { + private _body: string; + private _endpoint: string; + private _message: any; + private _messageType: Function; + + /** + * Creates a new OutgoingMessage instance. + * @param bus The bus instance associated with the message. + * @param message The message object to be sent. + */ + constructor(bus: IPolyBus, message: any, endpoint: string) { + super(bus); + this._message = message; + this._messageType = message?.constructor || Object; + this._body = message?.toString() || ''; + this._endpoint = endpoint; + } + + /** + * If the transport supports delayed messages, this is the time at which the message should be delivered. + */ + public deliverAt: Date | undefined; + + /** + * Gets the type of the message. + */ + public get messageType(): Function { + return this._messageType; + } + + /** + * Sets the type of the message. + */ + public set messageType(value: Function) { + this._messageType = value; + } + + /** + * The serialized message body contents. + */ + public get body(): string { + return this._body; + } + + /** + * Sets the serialized message body contents. + */ + public set body(value: string) { + this._body = value; + } + + /** + * If the message is a command then this is the endpoint the message is being sent to. + * If the message is an event then this is the source endpoint the message is being sent from. + */ + public get endpoint(): string { + return this._endpoint; + } + + /** + * Sets the endpoint for the message. + */ + public set endpoint(value: string) { + this._endpoint = value; + } + + /** + * The message object. + */ + public get message(): any { + return this._message; + } + + /** + * Sets the message object. + */ + public set message(value: any) { + this._message = value; + } +} diff --git a/src/typescript/src/transport/transaction/outgoing-transaction.ts b/src/typescript/src/transport/transaction/outgoing-transaction.ts new file mode 100644 index 0000000..38ad41b --- /dev/null +++ b/src/typescript/src/transport/transaction/outgoing-transaction.ts @@ -0,0 +1,16 @@ +import { IPolyBus } from '../../i-poly-bus'; +import { Transaction } from './transaction'; + +/** + * Represents an outgoing transaction in the transport layer. + * This is a simple transaction that extends the base Transaction class. + */ +export class OutgoingTransaction extends Transaction { + /** + * Creates a new OutgoingTransaction instance. + * @param bus The bus instance associated with the transaction. + */ + constructor(bus: IPolyBus) { + super(bus); + } +} diff --git a/src/typescript/src/transport/transaction/transaction-factory.ts b/src/typescript/src/transport/transaction/transaction-factory.ts new file mode 100644 index 0000000..e931f2f --- /dev/null +++ b/src/typescript/src/transport/transaction/transaction-factory.ts @@ -0,0 +1,11 @@ +import { IncomingMessage } from './message/incoming-message'; +import { IPolyBus } from '../../i-poly-bus'; +import { PolyBusBuilder } from '../../poly-bus-builder'; +import { Transaction } from './transaction'; + +/** + * A method for creating a new transaction for processing a request. + * This should be used to integrate with external transaction systems to ensure message processing + * is done within the context of a transaction. + */ +export type TransactionFactory = (builder: PolyBusBuilder, bus: IPolyBus, message?: IncomingMessage) => Promise; diff --git a/src/typescript/src/transport/transaction/transaction.ts b/src/typescript/src/transport/transaction/transaction.ts new file mode 100644 index 0000000..bc2a503 --- /dev/null +++ b/src/typescript/src/transport/transaction/transaction.ts @@ -0,0 +1,76 @@ +import { IPolyBus } from '../../i-poly-bus'; +import { OutgoingMessage } from './message/outgoing-message'; + +/** + * Represents a transaction in the transport layer. + * Contains the bus instance, state dictionary, and outgoing messages to be sent when committed. + */ +export class Transaction { + private readonly _bus: IPolyBus; + private readonly _state: Map = new Map(); + private readonly _outgoingMessages: OutgoingMessage[] = []; + + /** + * Creates a new Transaction instance. + * @param bus The bus instance associated with the transaction. + */ + constructor(bus: IPolyBus) { + if (!bus) { + throw new Error('Bus parameter cannot be null or undefined'); + } + this._bus = bus; + } + + /** + * The bus instance associated with the transaction. + */ + public get bus(): IPolyBus { + return this._bus; + } + + /** + * State dictionary that can be used to store arbitrary data associated with the transaction. + */ + public get state(): Map { + return this._state; + } + + /** + * A list of outgoing messages to be sent when the transaction is committed. + */ + public get outgoingMessages(): OutgoingMessage[] { + return this._outgoingMessages; + } + + /** + * Adds an outgoing message to be sent when the transaction is committed. + * @param message The message object to be sent. + * @returns The OutgoingMessage instance that was added. + */ + public addOutgoingMessage(message: any, endpoint: string | null = null): OutgoingMessage { + function getEndpoint(bus: IPolyBus): string { + const messageInfo = bus.messages.getMessageInfo(message.constructor); + if (!messageInfo) { + throw new Error(`Message type ${message.constructor.name} is not registered on bus ${bus.name}.`); + } + return messageInfo.endpoint; + } + const outgoingMessage = new OutgoingMessage(this._bus, message, endpoint ?? getEndpoint(this.bus)); + this._outgoingMessages.push(outgoingMessage); + return outgoingMessage; + } + + /** + * If an exception occurs during processing, the transaction will be aborted. + */ + public async abort(): Promise { + // Default implementation - can be overridden in subclasses + } + + /** + * If no exception occurs during processing, the transaction will be committed. + */ + public async commit(): Promise { + return this._bus.send(this); + } +} diff --git a/src/typescript/src/transport/transport-factory.ts b/src/typescript/src/transport/transport-factory.ts new file mode 100644 index 0000000..29350bb --- /dev/null +++ b/src/typescript/src/transport/transport-factory.ts @@ -0,0 +1,9 @@ +import { IPolyBus } from '../i-poly-bus'; +import { ITransport } from '../transport/i-transport'; +import { PolyBusBuilder } from '../poly-bus-builder'; + +/** + * Creates a transport instance to be used by PolyBus. + */ +// eslint-disable-next-line no-unused-vars +export type TransportFactory = (builder: PolyBusBuilder, bus: IPolyBus) => Promise; diff --git a/src/typescript/tsconfig.json b/src/typescript/tsconfig.json new file mode 100644 index 0000000..304d385 --- /dev/null +++ b/src/typescript/tsconfig.json @@ -0,0 +1,71 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "ES2018", + "lib": ["ES2018", "DOM"], + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "allowJs": false, + "checkJs": false, + + /* Output Options */ + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false, + "importHelpers": true, + + /* Strict Type-Checking Options */ + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noImplicitThis": true, + "alwaysStrict": true, + + /* Additional Checks */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": false, + + /* Module Resolution Options */ + "baseUrl": "./", + "paths": {}, + "typeRoots": ["node_modules/@types"], + "types": ["node"], + "resolveJsonModule": true, + + /* Advanced Options */ + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "preserveWatchOutput": true, + "pretty": true, + + /* Experimental Options */ + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts" + ], + "ts-node": { + "esm": true + } +} \ No newline at end of file