mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 11:36:44 +03:00
Updated some interfaces to be more future proof
This commit is contained in:
50
.env.example
50
.env.example
@@ -1,21 +1,40 @@
|
|||||||
# Docker Registry Configuration
|
# Gitea Mirror Configuration
|
||||||
DOCKER_REGISTRY=ghcr.io
|
# Copy this to .env and update with your values
|
||||||
DOCKER_IMAGE=arunavo4/gitea-mirror
|
|
||||||
DOCKER_TAG=latest
|
# ===========================================
|
||||||
|
# CORE CONFIGURATION
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
# Application Configuration
|
# Application Configuration
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=4321
|
PORT=4321
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
# For self-hosted, SQLite is used by default
|
||||||
DATABASE_URL=sqlite://data/gitea-mirror.db
|
DATABASE_URL=sqlite://data/gitea-mirror.db
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
|
# Generate with: openssl rand -base64 32
|
||||||
BETTER_AUTH_SECRET=change-this-to-a-secure-random-string-in-production
|
BETTER_AUTH_SECRET=change-this-to-a-secure-random-string-in-production
|
||||||
BETTER_AUTH_URL=http://localhost:4321
|
BETTER_AUTH_URL=http://localhost:4321
|
||||||
# ENCRYPTION_SECRET=optional-encryption-key-for-token-encryption # Generate with: openssl rand -base64 48
|
# ENCRYPTION_SECRET=optional-encryption-key-for-token-encryption # Generate with: openssl rand -base64 48
|
||||||
|
|
||||||
# Optional GitHub/Gitea Mirror Configuration (for docker-compose, can also be set via web UI)
|
# ===========================================
|
||||||
# Uncomment and set as needed. These are passed as environment variables to the container.
|
# DOCKER CONFIGURATION (Optional)
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Docker Registry Configuration
|
||||||
|
DOCKER_REGISTRY=ghcr.io
|
||||||
|
DOCKER_IMAGE=arunavo4/gitea-mirror
|
||||||
|
DOCKER_TAG=latest
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# MIRROR CONFIGURATION (Optional)
|
||||||
|
# Can also be configured via web UI
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# GitHub Configuration
|
||||||
# GITHUB_USERNAME=your-github-username
|
# GITHUB_USERNAME=your-github-username
|
||||||
# GITHUB_TOKEN=your-github-personal-access-token
|
# GITHUB_TOKEN=your-github-personal-access-token
|
||||||
# SKIP_FORKS=false
|
# SKIP_FORKS=false
|
||||||
@@ -27,6 +46,8 @@ BETTER_AUTH_URL=http://localhost:4321
|
|||||||
# PRESERVE_ORG_STRUCTURE=false
|
# PRESERVE_ORG_STRUCTURE=false
|
||||||
# ONLY_MIRROR_ORGS=false
|
# ONLY_MIRROR_ORGS=false
|
||||||
# SKIP_STARRED_ISSUES=false
|
# SKIP_STARRED_ISSUES=false
|
||||||
|
|
||||||
|
# Gitea Configuration
|
||||||
# GITEA_URL=http://gitea:3000
|
# GITEA_URL=http://gitea:3000
|
||||||
# GITEA_TOKEN=your-local-gitea-token
|
# GITEA_TOKEN=your-local-gitea-token
|
||||||
# GITEA_USERNAME=your-local-gitea-username
|
# GITEA_USERNAME=your-local-gitea-username
|
||||||
@@ -34,15 +55,14 @@ BETTER_AUTH_URL=http://localhost:4321
|
|||||||
# GITEA_ORG_VISIBILITY=public
|
# GITEA_ORG_VISIBILITY=public
|
||||||
# DELAY=3600
|
# DELAY=3600
|
||||||
|
|
||||||
# Optional Database Cleanup Configuration (configured via web UI)
|
# ===========================================
|
||||||
# These environment variables are optional and only used as defaults
|
# OPTIONAL FEATURES
|
||||||
# Users can configure cleanup settings through the web interface
|
# ===========================================
|
||||||
|
|
||||||
|
# Database Cleanup Configuration
|
||||||
# CLEANUP_ENABLED=false
|
# CLEANUP_ENABLED=false
|
||||||
# CLEANUP_RETENTION_DAYS=7
|
# CLEANUP_RETENTION_DAYS=7
|
||||||
|
|
||||||
# Optional TLS/SSL Configuration
|
# TLS/SSL Configuration
|
||||||
# Option 1: Mount custom CA certificates in ./certs directory as .crt files
|
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
|
||||||
# The container will automatically combine them into a CA bundle
|
|
||||||
# Option 2: Mount your system CA bundle at /etc/ssl/certs/ca-certificates.crt
|
|
||||||
# See docker-compose.yml for volume mount examples
|
|
||||||
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing, disables TLS verification
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,5 +32,3 @@ certs/*.pem
|
|||||||
certs/*.cer
|
certs/*.cer
|
||||||
!certs/README.md
|
!certs/README.md
|
||||||
|
|
||||||
# Hosted version documentation (local only)
|
|
||||||
docs/HOSTED_VERSION.md
|
|
||||||
|
|||||||
91
docs/SPONSOR_INTEGRATION.md
Normal file
91
docs/SPONSOR_INTEGRATION.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# GitHub Sponsors Integration
|
||||||
|
|
||||||
|
This guide shows how GitHub Sponsors is integrated into the open-source version of Gitea Mirror.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### GitHubSponsors Card
|
||||||
|
|
||||||
|
A card component that displays in the sidebar or dashboard:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { GitHubSponsors } from '@/components/sponsors/GitHubSponsors';
|
||||||
|
|
||||||
|
// In your layout or dashboard
|
||||||
|
<GitHubSponsors />
|
||||||
|
```
|
||||||
|
|
||||||
|
### SponsorButton
|
||||||
|
|
||||||
|
A smaller button for headers or navigation:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { SponsorButton } from '@/components/sponsors/GitHubSponsors';
|
||||||
|
|
||||||
|
// In your header
|
||||||
|
<SponsorButton />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
### 1. Dashboard Sidebar
|
||||||
|
|
||||||
|
Add the sponsor card to the dashboard sidebar for visibility:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/components/layout/DashboardLayout.tsx
|
||||||
|
<aside>
|
||||||
|
{/* Other sidebar content */}
|
||||||
|
<GitHubSponsors />
|
||||||
|
</aside>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Header Navigation
|
||||||
|
|
||||||
|
Add the sponsor button to the main navigation:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/components/layout/Header.tsx
|
||||||
|
<nav>
|
||||||
|
{/* Other nav items */}
|
||||||
|
<SponsorButton />
|
||||||
|
</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Settings Page
|
||||||
|
|
||||||
|
Add a support section in settings:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/components/settings/SupportSection.tsx
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Support Development</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<GitHubSponsors />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- **Only appears in self-hosted mode**: The components automatically hide in hosted mode
|
||||||
|
- **Non-intrusive**: Designed to be helpful without being annoying
|
||||||
|
- **Multiple options**: GitHub Sponsors, Buy Me a Coffee, and starring the repo
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
You can customize the sponsor components by:
|
||||||
|
|
||||||
|
1. Updating the GitHub Sponsors URL
|
||||||
|
2. Adding/removing donation platforms
|
||||||
|
3. Changing the styling to match your theme
|
||||||
|
4. Adjusting the placement based on user feedback
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Don't be pushy**: Show sponsor options tastefully
|
||||||
|
2. **Provide value first**: Ensure the tool is useful before asking for support
|
||||||
|
3. **Be transparent**: Explain how sponsorships help the project
|
||||||
|
4. **Thank sponsors**: Acknowledge supporters in README or releases
|
||||||
9087
package-lock.json
generated
Normal file
9087
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
72
src/components/layout/SponsorCard.tsx
Normal file
72
src/components/layout/SponsorCard.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Heart, Coffee, Sparkles } from "lucide-react";
|
||||||
|
import { isSelfHostedMode } from "@/lib/deployment-mode";
|
||||||
|
|
||||||
|
export function SponsorCard() {
|
||||||
|
// Only show in self-hosted mode
|
||||||
|
if (!isSelfHostedMode()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-auto p-4 border-t">
|
||||||
|
<Card className="bg-gradient-to-r from-purple-500/10 to-pink-500/10 border-purple-500/20">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Heart className="w-4 h-4 text-pink-500" />
|
||||||
|
Support Development
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-xs">
|
||||||
|
Help us improve Gitea Mirror
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Gitea Mirror is open source and free. Your sponsorship helps us maintain and improve it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Button
|
||||||
|
className="w-full h-8 text-xs"
|
||||||
|
size="sm"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://github.com/sponsors/RayLabsHQ"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Heart className="w-3 h-3 mr-2" />
|
||||||
|
Sponsor on GitHub
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full h-8 text-xs"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://buymeacoffee.com/raylabs"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Coffee className="w-3 h-3 mr-2" />
|
||||||
|
Buy us a coffee
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||||
|
<Sparkles className="w-3 h-3" />
|
||||||
|
Pro features available in hosted version
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
src/components/sponsors/GitHubSponsors.tsx
Normal file
105
src/components/sponsors/GitHubSponsors.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Heart, Coffee, Zap } from "lucide-react";
|
||||||
|
import { isSelfHostedMode } from "@/lib/deployment-mode";
|
||||||
|
|
||||||
|
export function GitHubSponsors() {
|
||||||
|
// Only show in self-hosted mode
|
||||||
|
if (!isSelfHostedMode()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 border-purple-200 dark:border-purple-800">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-purple-900 dark:text-purple-100">
|
||||||
|
<Heart className="w-5 h-5 text-pink-500" />
|
||||||
|
Support Gitea Mirror
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-sm text-purple-800 dark:text-purple-200">
|
||||||
|
Gitea Mirror is open source and free to use. If you find it helpful,
|
||||||
|
consider supporting the project!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://github.com/sponsors/RayLabsHQ"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Heart className="w-4 h-4 mr-2" />
|
||||||
|
Become a Sponsor
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-purple-300 hover:bg-purple-100 dark:border-purple-700 dark:hover:bg-purple-900"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://github.com/RayLabsHQ/gitea-mirror"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
⭐ Star on GitHub
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-purple-300 hover:bg-purple-100 dark:border-purple-700 dark:hover:bg-purple-900"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://buymeacoffee.com/raylabs"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Coffee className="w-4 h-4 mr-1" />
|
||||||
|
Buy Coffee
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-purple-600 dark:text-purple-300 space-y-1">
|
||||||
|
<p className="flex items-center gap-1">
|
||||||
|
<Zap className="w-3 h-3" />
|
||||||
|
Your support helps maintain and improve the project
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smaller inline sponsor button for headers/navbars
|
||||||
|
export function SponsorButton() {
|
||||||
|
if (!isSelfHostedMode()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button variant="outline" size="sm" asChild>
|
||||||
|
<a
|
||||||
|
href="https://github.com/sponsors/RayLabsHQ"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Heart className="w-4 h-4 mr-2" />
|
||||||
|
Sponsor
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { oidcProvider } from "better-auth/plugins";
|
import { oidcProvider, sso } from "better-auth/plugins";
|
||||||
import { sso } from "better-auth/plugins/sso";
|
|
||||||
import { db, users } from "./db";
|
import { db, users } from "./db";
|
||||||
import * as schema from "./db/schema";
|
import * as schema from "./db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|||||||
102
src/lib/db/adapter.ts
Normal file
102
src/lib/db/adapter.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Database adapter for SQLite
|
||||||
|
* For the self-hosted version of Gitea Mirror
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { drizzle as drizzleSqlite } from 'drizzle-orm/bun-sqlite';
|
||||||
|
import { Database } from 'bun:sqlite';
|
||||||
|
import * as schema from './schema';
|
||||||
|
|
||||||
|
export type DatabaseClient = ReturnType<typeof createDatabase>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SQLite database connection
|
||||||
|
*/
|
||||||
|
export function createDatabase() {
|
||||||
|
const dbPath = process.env.DATABASE_PATH || './data/gitea-mirror.db';
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const dir = path.dirname(dbPath);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create SQLite connection
|
||||||
|
const sqlite = new Database(dbPath);
|
||||||
|
|
||||||
|
// Enable foreign keys and WAL mode for better performance
|
||||||
|
sqlite.exec('PRAGMA foreign_keys = ON');
|
||||||
|
sqlite.exec('PRAGMA journal_mode = WAL');
|
||||||
|
sqlite.exec('PRAGMA synchronous = NORMAL');
|
||||||
|
sqlite.exec('PRAGMA cache_size = -2000'); // 2MB cache
|
||||||
|
sqlite.exec('PRAGMA temp_store = MEMORY');
|
||||||
|
|
||||||
|
// Create Drizzle instance with SQLite
|
||||||
|
const db = drizzleSqlite(sqlite, {
|
||||||
|
schema,
|
||||||
|
logger: process.env.NODE_ENV === 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
db,
|
||||||
|
client: sqlite,
|
||||||
|
type: 'sqlite' as const,
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
async close() {
|
||||||
|
sqlite.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
async healthCheck() {
|
||||||
|
try {
|
||||||
|
sqlite.query('SELECT 1').get();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async transaction<T>(fn: (tx: any) => Promise<T>) {
|
||||||
|
return db.transaction(fn);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
let dbInstance: DatabaseClient | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database instance (singleton)
|
||||||
|
*/
|
||||||
|
export function getDatabase(): DatabaseClient {
|
||||||
|
if (!dbInstance) {
|
||||||
|
dbInstance = createDatabase();
|
||||||
|
}
|
||||||
|
return dbInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close database connection
|
||||||
|
*/
|
||||||
|
export async function closeDatabase() {
|
||||||
|
if (dbInstance) {
|
||||||
|
await dbInstance.close();
|
||||||
|
dbInstance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export convenience references
|
||||||
|
export const { db, client, type: dbType } = getDatabase();
|
||||||
|
|
||||||
|
// Re-export schema for convenience
|
||||||
|
export * from './schema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database migration utilities
|
||||||
|
*/
|
||||||
|
export async function runMigrations() {
|
||||||
|
const { migrate } = await import('drizzle-orm/bun-sqlite/migrator');
|
||||||
|
await migrate(db, { migrationsFolder: './drizzle' });
|
||||||
|
}
|
||||||
21
src/lib/deployment-mode.ts
Normal file
21
src/lib/deployment-mode.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Deployment mode utilities
|
||||||
|
* For the open source self-hosted version
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DEPLOYMENT_MODE = 'selfhosted';
|
||||||
|
|
||||||
|
export const isSelfHostedMode = () => true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature flags for self-hosted version
|
||||||
|
*/
|
||||||
|
export const features = {
|
||||||
|
// Core features available
|
||||||
|
githubSync: true,
|
||||||
|
giteaMirroring: true,
|
||||||
|
scheduling: true,
|
||||||
|
multiUser: true,
|
||||||
|
githubSponsors: true,
|
||||||
|
unlimitedRepos: true,
|
||||||
|
};
|
||||||
256
src/lib/events/realtime.ts
Normal file
256
src/lib/events/realtime.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
/**
|
||||||
|
* Real-time event system using EventEmitter
|
||||||
|
* For the self-hosted version
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
export interface RealtimeEvent {
|
||||||
|
type: string;
|
||||||
|
userId?: string;
|
||||||
|
data: any;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Real-time event bus for local instance
|
||||||
|
*/
|
||||||
|
export class RealtimeEventBus extends EventEmitter {
|
||||||
|
private channels = new Map<string, Set<(event: RealtimeEvent) => void>>();
|
||||||
|
private userChannels = new Map<string, string[]>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming events
|
||||||
|
*/
|
||||||
|
private handleIncomingEvent(channel: string, event: RealtimeEvent) {
|
||||||
|
// Emit to local listeners
|
||||||
|
this.emit(channel, event);
|
||||||
|
|
||||||
|
// Call channel-specific handlers
|
||||||
|
const handlers = this.channels.get(channel);
|
||||||
|
if (handlers) {
|
||||||
|
handlers.forEach(handler => {
|
||||||
|
try {
|
||||||
|
handler(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in event handler:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to a channel
|
||||||
|
*/
|
||||||
|
async subscribe(channel: string, handler?: (event: RealtimeEvent) => void) {
|
||||||
|
// Add handler if provided
|
||||||
|
if (handler) {
|
||||||
|
if (!this.channels.has(channel)) {
|
||||||
|
this.channels.set(channel, new Set());
|
||||||
|
}
|
||||||
|
this.channels.get(channel)!.add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add local listener
|
||||||
|
if (!this.listenerCount(channel)) {
|
||||||
|
this.on(channel, (event) => this.handleIncomingEvent(channel, event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to user-specific channels
|
||||||
|
*/
|
||||||
|
async subscribeUser(userId: string) {
|
||||||
|
const channels = [
|
||||||
|
`user:${userId}`,
|
||||||
|
`user:${userId}:notifications`,
|
||||||
|
`user:${userId}:updates`,
|
||||||
|
];
|
||||||
|
|
||||||
|
this.userChannels.set(userId, channels);
|
||||||
|
|
||||||
|
for (const channel of channels) {
|
||||||
|
await this.subscribe(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from a channel
|
||||||
|
*/
|
||||||
|
async unsubscribe(channel: string, handler?: (event: RealtimeEvent) => void) {
|
||||||
|
// Remove handler if provided
|
||||||
|
if (handler) {
|
||||||
|
this.channels.get(channel)?.delete(handler);
|
||||||
|
|
||||||
|
// Remove channel if no handlers left
|
||||||
|
if (this.channels.get(channel)?.size === 0) {
|
||||||
|
this.channels.delete(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove local listener if no handlers
|
||||||
|
if (!this.channels.has(channel)) {
|
||||||
|
this.removeAllListeners(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from user channels
|
||||||
|
*/
|
||||||
|
async unsubscribeUser(userId: string) {
|
||||||
|
const channels = this.userChannels.get(userId) || [];
|
||||||
|
|
||||||
|
for (const channel of channels) {
|
||||||
|
await this.unsubscribe(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.userChannels.delete(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish an event
|
||||||
|
*/
|
||||||
|
async publish(channel: string, event: Omit<RealtimeEvent, 'timestamp'>) {
|
||||||
|
const fullEvent: RealtimeEvent = {
|
||||||
|
...event,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit locally
|
||||||
|
this.handleIncomingEvent(channel, fullEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast to all users
|
||||||
|
*/
|
||||||
|
async broadcast(event: Omit<RealtimeEvent, 'timestamp'>) {
|
||||||
|
await this.publish('broadcast', event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send event to specific user
|
||||||
|
*/
|
||||||
|
async sendToUser(userId: string, event: Omit<RealtimeEvent, 'timestamp' | 'userId'>) {
|
||||||
|
await this.publish(`user:${userId}`, {
|
||||||
|
...event,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send activity update
|
||||||
|
*/
|
||||||
|
async sendActivity(activity: {
|
||||||
|
userId: string;
|
||||||
|
action: string;
|
||||||
|
resource: string;
|
||||||
|
resourceId: string;
|
||||||
|
details?: any;
|
||||||
|
}) {
|
||||||
|
const event = {
|
||||||
|
type: 'activity',
|
||||||
|
data: activity,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send to user
|
||||||
|
await this.sendToUser(activity.userId, event);
|
||||||
|
|
||||||
|
// Also publish to activity channel
|
||||||
|
await this.publish('activity', {
|
||||||
|
...event,
|
||||||
|
userId: activity.userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get event statistics
|
||||||
|
*/
|
||||||
|
getStats() {
|
||||||
|
return {
|
||||||
|
channels: this.channels.size,
|
||||||
|
listeners: Array.from(this.channels.values()).reduce(
|
||||||
|
(sum, handlers) => sum + handlers.size,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
userChannels: this.userChannels.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global event bus instance
|
||||||
|
export const eventBus = new RealtimeEventBus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook for subscribing to events
|
||||||
|
*/
|
||||||
|
export function useRealtimeEvents(
|
||||||
|
channel: string,
|
||||||
|
handler: (event: RealtimeEvent) => void,
|
||||||
|
deps: any[] = []
|
||||||
|
) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const { useEffect } = require('react');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
eventBus.subscribe(channel, handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventBus.unsubscribe(channel, handler);
|
||||||
|
};
|
||||||
|
}, deps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-sent events endpoint handler
|
||||||
|
*/
|
||||||
|
export async function createSSEHandler(userId: string) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
|
||||||
|
// Create a readable stream for SSE
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
// Send initial connection event
|
||||||
|
controller.enqueue(
|
||||||
|
encoder.encode(`data: ${JSON.stringify({ type: 'connected' })}\n\n`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subscribe to user channels
|
||||||
|
await eventBus.subscribeUser(userId);
|
||||||
|
|
||||||
|
// Create event handler
|
||||||
|
const handleEvent = (event: RealtimeEvent) => {
|
||||||
|
controller.enqueue(
|
||||||
|
encoder.encode(`data: ${JSON.stringify(event)}\n\n`)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Subscribe to channels
|
||||||
|
eventBus.on(`user:${userId}`, handleEvent);
|
||||||
|
|
||||||
|
// Keep connection alive with heartbeat
|
||||||
|
const heartbeat = setInterval(() => {
|
||||||
|
controller.enqueue(encoder.encode(': heartbeat\n\n'));
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// Cleanup on close
|
||||||
|
return () => {
|
||||||
|
clearInterval(heartbeat);
|
||||||
|
eventBus.off(`user:${userId}`, handleEvent);
|
||||||
|
eventBus.unsubscribeUser(userId);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
184
src/lib/modules/registry.ts
Normal file
184
src/lib/modules/registry.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Module registry implementation
|
||||||
|
* Manages loading and access to modular features
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Module,
|
||||||
|
ModuleRegistry,
|
||||||
|
AppContext,
|
||||||
|
RouteHandler,
|
||||||
|
Middleware,
|
||||||
|
DatabaseAdapter,
|
||||||
|
EventEmitter
|
||||||
|
} from './types';
|
||||||
|
// Module registry for extensibility
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple event emitter implementation
|
||||||
|
*/
|
||||||
|
class SimpleEventEmitter implements EventEmitter {
|
||||||
|
private events: Map<string, Set<Function>> = new Map();
|
||||||
|
|
||||||
|
on(event: string, handler: (...args: any[]) => void): void {
|
||||||
|
if (!this.events.has(event)) {
|
||||||
|
this.events.set(event, new Set());
|
||||||
|
}
|
||||||
|
this.events.get(event)!.add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: string, handler: (...args: any[]) => void): void {
|
||||||
|
this.events.get(event)?.delete(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: string, ...args: any[]): void {
|
||||||
|
this.events.get(event)?.forEach(handler => {
|
||||||
|
try {
|
||||||
|
handler(...args);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in event handler for ${event}:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module manager class
|
||||||
|
*/
|
||||||
|
export class ModuleManager {
|
||||||
|
private modules: Map<string, Module> = new Map();
|
||||||
|
private routes: Map<string, RouteHandler> = new Map();
|
||||||
|
private middlewares: Middleware[] = [];
|
||||||
|
private events = new SimpleEventEmitter();
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app context for modules
|
||||||
|
*/
|
||||||
|
private getAppContext(): AppContext {
|
||||||
|
return {
|
||||||
|
addRoute: (path, handler) => this.addRoute(path, handler),
|
||||||
|
addMiddleware: (middleware) => this.middlewares.push(middleware),
|
||||||
|
db: this.getDatabaseAdapter(),
|
||||||
|
events: this.events,
|
||||||
|
modules: this.getRegistry(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database adapter based on deployment mode
|
||||||
|
*/
|
||||||
|
private getDatabaseAdapter(): DatabaseAdapter {
|
||||||
|
// This would be implemented to use SQLite or PostgreSQL
|
||||||
|
// based on deployment mode
|
||||||
|
return {
|
||||||
|
query: async (sql, params) => [],
|
||||||
|
execute: async (sql, params) => {},
|
||||||
|
transaction: async (fn) => fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a module
|
||||||
|
*/
|
||||||
|
async register(module: Module): Promise<void> {
|
||||||
|
if (this.modules.has(module.name)) {
|
||||||
|
console.warn(`Module ${module.name} is already registered`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await module.init(this.getAppContext());
|
||||||
|
this.modules.set(module.name, module);
|
||||||
|
console.log(`Module ${module.name} registered successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to register module ${module.name}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister a module
|
||||||
|
*/
|
||||||
|
async unregister(moduleName: string): Promise<void> {
|
||||||
|
const module = this.modules.get(moduleName);
|
||||||
|
if (!module) return;
|
||||||
|
|
||||||
|
if (module.cleanup) {
|
||||||
|
await module.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modules.delete(moduleName);
|
||||||
|
// Remove routes registered by this module
|
||||||
|
// This would need to track which module registered which routes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a route handler
|
||||||
|
*/
|
||||||
|
private addRoute(path: string, handler: RouteHandler): void {
|
||||||
|
this.routes.set(path, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get route handler for a path
|
||||||
|
*/
|
||||||
|
getRouteHandler(path: string): RouteHandler | null {
|
||||||
|
return this.routes.get(path) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all middleware
|
||||||
|
*/
|
||||||
|
getMiddleware(): Middleware[] {
|
||||||
|
return [...this.middlewares];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get module registry
|
||||||
|
*/
|
||||||
|
getRegistry(): ModuleRegistry {
|
||||||
|
const registry: ModuleRegistry = {};
|
||||||
|
|
||||||
|
// Copy all modules to registry
|
||||||
|
for (const [name, module] of this.modules) {
|
||||||
|
registry[name] = module;
|
||||||
|
}
|
||||||
|
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific module
|
||||||
|
*/
|
||||||
|
get<K extends keyof ModuleRegistry>(name: K): ModuleRegistry[K] | null {
|
||||||
|
return this.getRegistry()[name] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a module is loaded
|
||||||
|
*/
|
||||||
|
has(name: string): boolean {
|
||||||
|
return this.modules.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit an event to all modules
|
||||||
|
*/
|
||||||
|
emit(event: string, ...args: any[]): void {
|
||||||
|
this.events.emit(event, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global module manager instance
|
||||||
|
export const modules = new ModuleManager();
|
||||||
|
|
||||||
|
|
||||||
|
// Initialize modules on app start
|
||||||
|
export async function initializeModules() {
|
||||||
|
// Load core modules here if any
|
||||||
|
|
||||||
|
// Emit initialization complete event
|
||||||
|
modules.emit('modules:initialized');
|
||||||
|
}
|
||||||
86
src/lib/modules/types.d.ts
vendored
Normal file
86
src/lib/modules/types.d.ts
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Module system type definitions
|
||||||
|
* These interfaces allow for extensibility and plugins
|
||||||
|
*/
|
||||||
|
import type { APIContext } from 'astro';
|
||||||
|
import type { ComponentType, LazyExoticComponent } from 'react';
|
||||||
|
/**
|
||||||
|
* Base module interface that all modules must implement
|
||||||
|
*/
|
||||||
|
export interface Module {
|
||||||
|
/** Unique module identifier */
|
||||||
|
name: string;
|
||||||
|
/** Module version */
|
||||||
|
version: string;
|
||||||
|
/** Initialize the module with app context */
|
||||||
|
init(app: AppContext): Promise<void>;
|
||||||
|
/** Cleanup when module is unloaded */
|
||||||
|
cleanup?(): Promise<void>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Application context passed to modules
|
||||||
|
*/
|
||||||
|
export interface AppContext {
|
||||||
|
/** Register API routes */
|
||||||
|
addRoute(path: string, handler: RouteHandler): void;
|
||||||
|
/** Register middleware */
|
||||||
|
addMiddleware(middleware: Middleware): void;
|
||||||
|
/** Access to database (abstracted) */
|
||||||
|
db: DatabaseAdapter;
|
||||||
|
/** Event emitter for cross-module communication */
|
||||||
|
events: EventEmitter;
|
||||||
|
/** Access to other modules */
|
||||||
|
modules: ModuleRegistry;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Route handler type
|
||||||
|
*/
|
||||||
|
export type RouteHandler = (context: APIContext) => Promise<Response> | Response;
|
||||||
|
/**
|
||||||
|
* Middleware type
|
||||||
|
*/
|
||||||
|
export type Middleware = (context: APIContext, next: () => Promise<Response>) => Promise<Response>;
|
||||||
|
/**
|
||||||
|
* Database adapter interface (abstract away implementation)
|
||||||
|
*/
|
||||||
|
export interface DatabaseAdapter {
|
||||||
|
query<T>(sql: string, params?: any[]): Promise<T[]>;
|
||||||
|
execute(sql: string, params?: any[]): Promise<void>;
|
||||||
|
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Event emitter for cross-module communication
|
||||||
|
*/
|
||||||
|
export interface EventEmitter {
|
||||||
|
on(event: string, handler: (...args: any[]) => void): void;
|
||||||
|
off(event: string, handler: (...args: any[]) => void): void;
|
||||||
|
emit(event: string, ...args: any[]): void;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Example module interfaces
|
||||||
|
* These are examples of how modules can be structured
|
||||||
|
*/
|
||||||
|
export interface FeatureModule extends Module {
|
||||||
|
/** React components provided by the module */
|
||||||
|
components?: Record<string, LazyExoticComponent<ComponentType<any>>>;
|
||||||
|
/** API methods provided by the module */
|
||||||
|
api?: Record<string, (...args: any[]) => Promise<any>>;
|
||||||
|
/** Lifecycle hooks */
|
||||||
|
hooks?: {
|
||||||
|
onInit?: () => Promise<void>;
|
||||||
|
onUserAction?: (action: string, data: any) => Promise<void>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Module registry interface
|
||||||
|
*/
|
||||||
|
export interface ModuleRegistry {
|
||||||
|
[key: string]: Module | undefined;
|
||||||
|
}
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=types.d.ts.map
|
||||||
1
src/lib/modules/types.d.ts.map
Normal file
1
src/lib/modules/types.d.ts.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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"}
|
||||||
5
src/lib/modules/types.js
Normal file
5
src/lib/modules/types.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Module system type definitions
|
||||||
|
* These interfaces allow for extensibility and plugins
|
||||||
|
*/
|
||||||
|
export {};
|
||||||
110
src/lib/modules/types.ts
Normal file
110
src/lib/modules/types.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Module system type definitions
|
||||||
|
* These interfaces allow for extensibility and plugins
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { APIContext } from 'astro';
|
||||||
|
import type { ComponentType, LazyExoticComponent } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base module interface that all modules must implement
|
||||||
|
*/
|
||||||
|
export interface Module {
|
||||||
|
/** Unique module identifier */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Module version */
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
/** Initialize the module with app context */
|
||||||
|
init(app: AppContext): Promise<void>;
|
||||||
|
|
||||||
|
/** Cleanup when module is unloaded */
|
||||||
|
cleanup?(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application context passed to modules
|
||||||
|
*/
|
||||||
|
export interface AppContext {
|
||||||
|
/** Register API routes */
|
||||||
|
addRoute(path: string, handler: RouteHandler): void;
|
||||||
|
|
||||||
|
/** Register middleware */
|
||||||
|
addMiddleware(middleware: Middleware): void;
|
||||||
|
|
||||||
|
/** Access to database (abstracted) */
|
||||||
|
db: DatabaseAdapter;
|
||||||
|
|
||||||
|
/** Event emitter for cross-module communication */
|
||||||
|
events: EventEmitter;
|
||||||
|
|
||||||
|
/** Access to other modules */
|
||||||
|
modules: ModuleRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route handler type
|
||||||
|
*/
|
||||||
|
export type RouteHandler = (context: APIContext) => Promise<Response> | Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware type
|
||||||
|
*/
|
||||||
|
export type Middleware = (
|
||||||
|
context: APIContext,
|
||||||
|
next: () => Promise<Response>
|
||||||
|
) => Promise<Response>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database adapter interface (abstract away implementation)
|
||||||
|
*/
|
||||||
|
export interface DatabaseAdapter {
|
||||||
|
query<T>(sql: string, params?: any[]): Promise<T[]>;
|
||||||
|
execute(sql: string, params?: any[]): Promise<void>;
|
||||||
|
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitter for cross-module communication
|
||||||
|
*/
|
||||||
|
export interface EventEmitter {
|
||||||
|
on(event: string, handler: (...args: any[]) => void): void;
|
||||||
|
off(event: string, handler: (...args: any[]) => void): void;
|
||||||
|
emit(event: string, ...args: any[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example module interfaces
|
||||||
|
* These are examples of how modules can be structured
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example: Feature module with components
|
||||||
|
export interface FeatureModule extends Module {
|
||||||
|
/** React components provided by the module */
|
||||||
|
components?: Record<string, LazyExoticComponent<ComponentType<any>>>;
|
||||||
|
|
||||||
|
/** API methods provided by the module */
|
||||||
|
api?: Record<string, (...args: any[]) => Promise<any>>;
|
||||||
|
|
||||||
|
/** Lifecycle hooks */
|
||||||
|
hooks?: {
|
||||||
|
onInit?: () => Promise<void>;
|
||||||
|
onUserAction?: (action: string, data: any) => Promise<void>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module registry interface
|
||||||
|
*/
|
||||||
|
export interface ModuleRegistry {
|
||||||
|
[key: string]: Module | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic types that modules might use
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user