brainy/src/distributed/readWriteSeparation.ts
David Snelling fcb7197fb0 feat: add node: protocol to all Node.js built-in imports for bundler compatibility
- Updated all fs, path, crypto, os, url, util, events, http, https, net, child_process, stream, and zlib imports
- Changed both static imports and dynamic imports to use node: protocol
- This makes Brainy more bundler-friendly by explicitly marking Node.js built-ins
- Prevents bundlers from attempting to polyfill or bundle these modules
- Reduces bundle size for web applications using Brainy
- Improves tree-shaking and dead code elimination

Benefits for external bundlers:
- Clear distinction between Node.js built-ins and external dependencies
- No ambiguity about what needs polyfilling
- Smaller bundles for browser builds
- Better compatibility with modern bundlers (Webpack 5, Vite, Rollup, esbuild)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 14:20:21 -07:00

457 lines
No EOL
11 KiB
TypeScript

/**
* Read/Write Separation for Distributed Scaling
* Implements primary-replica architecture for scalable reads
*/
import { EventEmitter } from 'node:events'
import { DistributedCoordinator } from './coordinator.js'
import { ShardManager } from './shardManager.js'
import { CacheSync } from './cacheSync.js'
export interface ReplicationConfig {
nodeId: string
role?: 'primary' | 'replica' | 'auto'
primaryUrl?: string
replicaUrls?: string[]
syncInterval?: number
readPreference?: 'primary' | 'replica' | 'nearest'
consistencyLevel?: 'eventual' | 'strong' | 'bounded'
maxStaleness?: number
}
export interface WriteOperation {
id: string
type: 'add' | 'update' | 'delete'
data: any
timestamp: number
version: number
}
export interface ReplicationLog {
operations: WriteOperation[]
lastSequence: number
primaryVersion: number
}
/**
* Read/Write Separation Manager
*/
export class ReadWriteSeparation extends EventEmitter {
private nodeId: string
private role: 'primary' | 'replica'
private coordinator: DistributedCoordinator
private cacheSync: CacheSync
private replicationLog: ReplicationLog
private replicas: Map<string, ReplicaConnection> = new Map()
private primaryConnection?: PrimaryConnection
private config: ReplicationConfig
private syncTimer?: NodeJS.Timeout
private isRunning: boolean = false
constructor(
config: ReplicationConfig,
coordinator: DistributedCoordinator,
_shardManager: ShardManager,
cacheSync: CacheSync
) {
super()
this.config = config
this.nodeId = config.nodeId
this.role = config.role === 'auto' ? this.determineRole() : (config.role || 'replica')
this.coordinator = coordinator
this.cacheSync = cacheSync
this.replicationLog = {
operations: [],
lastSequence: 0,
primaryVersion: 0
}
// Setup connections based on role
this.setupConnections()
}
/**
* Start read/write separation
*/
async start(): Promise<void> {
if (this.isRunning) return
this.isRunning = true
// Start components
await this.coordinator.start()
// Setup role-specific behavior
if (this.role === 'primary') {
this.startAsPrimary()
} else {
this.startAsReplica()
}
this.emit('started', { nodeId: this.nodeId, role: this.role })
}
/**
* Stop read/write separation
*/
async stop(): Promise<void> {
if (!this.isRunning) return
this.isRunning = false
if (this.syncTimer) {
clearInterval(this.syncTimer)
this.syncTimer = undefined
}
await this.coordinator.stop()
this.emit('stopped', { nodeId: this.nodeId })
}
/**
* Execute a write operation (primary only)
*/
async write(operation: Omit<WriteOperation, 'id' | 'timestamp' | 'version'>): Promise<string> {
if (this.role !== 'primary') {
// Forward to primary
if (this.primaryConnection) {
return this.primaryConnection.forwardWrite(operation)
}
throw new Error('Cannot write: not connected to primary')
}
// Generate operation metadata
const writeOp: WriteOperation = {
id: this.generateOperationId(),
...operation,
timestamp: Date.now(),
version: ++this.replicationLog.primaryVersion
}
// Add to replication log
this.replicationLog.operations.push(writeOp)
this.replicationLog.lastSequence++
// Propagate to replicas
this.propagateToReplicas(writeOp)
// Update cache
if (operation.type !== 'delete') {
this.cacheSync.set(writeOp.id, operation.data)
} else {
this.cacheSync.delete(writeOp.id)
}
this.emit('write', writeOp)
return writeOp.id
}
/**
* Execute a read operation
*/
async read(key: string, options?: { consistency?: 'eventual' | 'strong' }): Promise<any> {
const consistency = options?.consistency || this.config.consistencyLevel || 'eventual'
if (consistency === 'strong' && this.role === 'replica') {
// For strong consistency, read from primary
if (this.primaryConnection) {
return this.primaryConnection.read(key)
}
throw new Error('Cannot guarantee strong consistency: not connected to primary')
}
// Check cache first
const cached = this.cacheSync.get(key)
if (cached !== undefined) {
return cached
}
// Read from appropriate source based on preference
if (this.config.readPreference === 'primary' && this.primaryConnection) {
return this.primaryConnection.read(key)
}
// Read locally (replica or primary)
return this.readLocal(key)
}
/**
* Get replication lag (replica only)
*/
getReplicationLag(): number {
if (this.role === 'primary') return 0
if (this.primaryConnection) {
return Date.now() - this.primaryConnection.lastSync
}
return -1 // Unknown
}
/**
* Setup connections based on role
*/
private setupConnections(): void {
if (this.role === 'primary') {
// Setup replica connections
if (this.config.replicaUrls) {
for (const url of this.config.replicaUrls) {
const replica = new ReplicaConnection(url)
this.replicas.set(url, replica)
}
}
} else {
// Setup primary connection
if (this.config.primaryUrl) {
this.primaryConnection = new PrimaryConnection(this.config.primaryUrl)
}
}
}
/**
* Start as primary node
*/
private startAsPrimary(): void {
// Start accepting writes
this.emit('roleEstablished', { role: 'primary' })
// Start replication timer
this.syncTimer = setInterval(() => {
this.syncReplicas()
}, this.config.syncInterval || 1000)
}
/**
* Start as replica node
*/
private startAsReplica(): void {
// Start syncing from primary
this.emit('roleEstablished', { role: 'replica' })
// Start sync timer
this.syncTimer = setInterval(() => {
this.syncFromPrimary()
}, this.config.syncInterval || 1000)
}
/**
* Sync replicas (primary only)
*/
private async syncReplicas(): Promise<void> {
const batch = this.replicationLog.operations.slice(-100) // Last 100 ops
for (const [url, replica] of this.replicas) {
try {
await replica.sync(batch, this.replicationLog.primaryVersion)
} catch (error) {
this.emit('replicaSyncError', { url, error })
}
}
}
/**
* Sync from primary (replica only)
*/
private async syncFromPrimary(): Promise<void> {
if (!this.primaryConnection) return
try {
const updates = await this.primaryConnection.getUpdates(
this.replicationLog.lastSequence
)
// Apply updates
for (const op of updates) {
await this.applyOperation(op)
}
this.emit('synced', { operations: updates.length })
} catch (error) {
this.emit('primarySyncError', { error })
}
}
/**
* Apply a replicated operation
*/
private async applyOperation(op: WriteOperation): Promise<void> {
// Update local state
this.replicationLog.operations.push(op)
this.replicationLog.lastSequence = Math.max(
this.replicationLog.lastSequence,
op.version
)
// Update cache
switch (op.type) {
case 'add':
case 'update':
this.cacheSync.set(op.id, op.data)
break
case 'delete':
this.cacheSync.delete(op.id)
break
}
this.emit('operationApplied', op)
}
/**
* Propagate operation to replicas
*/
private propagateToReplicas(op: WriteOperation): void {
for (const replica of this.replicas.values()) {
replica.sendOperation(op).catch(error => {
this.emit('replicationError', { replica, error })
})
}
}
/**
* Determine role automatically
*/
private determineRole(): 'primary' | 'replica' {
// Use coordinator's leader election
return this.coordinator.isLeader() ? 'primary' : 'replica'
}
/**
* Read from local storage
*/
private async readLocal(key: string): Promise<any> {
// This would connect to actual storage
// For now, return from cache or undefined
return this.cacheSync.get(key)
}
/**
* Generate unique operation ID
*/
private generateOperationId(): string {
return `${this.nodeId}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`
}
/**
* Get replication statistics
*/
getStats(): {
role: string
replicas: number
replicationLag: number
operationsInLog: number
primaryVersion: number
} {
return {
role: this.role,
replicas: this.replicas.size,
replicationLag: this.getReplicationLag(),
operationsInLog: this.replicationLog.operations.length,
primaryVersion: this.replicationLog.primaryVersion
}
}
/**
* Check if node can accept writes
*/
canWrite(): boolean {
return this.role === 'primary'
}
/**
* Check if node can serve reads
*/
canRead(): boolean {
if (this.config.consistencyLevel === 'strong') {
return this.role === 'primary' || this.primaryConnection !== undefined
}
return true
}
/**
* Set whether this node is primary (for leader election integration)
*/
setPrimary(isPrimary: boolean): void {
const newRole = isPrimary ? 'primary' : 'replica'
if (this.role !== newRole) {
this.role = newRole
this.emit('roleChange', { oldRole: this.role, newRole })
if (isPrimary) {
// Became primary - stop syncing from old primary
this.primaryConnection = undefined
} else {
// Became replica - connect to new primary if URL is known
if (this.config.primaryUrl) {
this.primaryConnection = new PrimaryConnection(this.config.primaryUrl)
}
}
}
}
}
/**
* Connection to a replica (used by primary)
*/
class ReplicaConnection {
constructor(private url: string) {
// Store URL for connection
void this.url
}
async sync(_operations: WriteOperation[], _version: number): Promise<void> {
// In real implementation, this would use HTTP/gRPC to this.url
// For now, simulate network call
await new Promise(resolve => setTimeout(resolve, 10))
}
async sendOperation(_op: WriteOperation): Promise<void> {
// Send single operation to replica at this.url
await new Promise(resolve => setTimeout(resolve, 5))
}
}
/**
* Connection to primary (used by replicas)
*/
class PrimaryConnection {
lastSync: number = Date.now()
constructor(private url: string) {
// Store URL for connection
void this.url
}
async getUpdates(_fromSequence: number): Promise<WriteOperation[]> {
// In real implementation, fetch from primary at this.url
this.lastSync = Date.now()
return []
}
async forwardWrite(_operation: any): Promise<string> {
// Forward write to primary at this.url
await new Promise(resolve => setTimeout(resolve, 20))
return `forwarded-${Date.now()}`
}
async read(_key: string): Promise<any> {
// Read from primary at this.url for strong consistency
await new Promise(resolve => setTimeout(resolve, 10))
return undefined
}
}
/**
* Create read/write separation manager
*/
export function createReadWriteSeparation(
config: ReplicationConfig,
coordinator: DistributedCoordinator,
shardManager: ShardManager,
cacheSync: CacheSync
): ReadWriteSeparation {
return new ReadWriteSeparation(config, coordinator, shardManager, cacheSync)
}