Docs/TuffTransport Internals
Universal Developer

TuffTransport Internals

TuffTransport Internals

Overview

This document provides a deep dive into TuffTransport's architecture, explaining the technical decisions and implementation details.

Introduction

Read this with the TuffTransport API to understand how the runtime and IPC layers connect.

Technical Notes

Architecture overview

EXAMPLE.VUE
┌─────────────────────────────────────────────────────────────────────────┐
│                        TuffTransport Architecture                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   Plugin Renderer                           Main Process                 │
│   ┌─────────────────────┐                  ┌─────────────────────┐      │
│   │  useTuffTransport() │                  │  TuffTransportMain  │      │
│   │  ┌───────────────┐  │                  │  ┌───────────────┐  │      │
│   │  │ Event Builder │  │                  │  │ Event Router  │  │      │
│   │  └───────┬───────┘  │                  │  └───────┬───────┘  │      │
│   │          │          │                  │          │          │      │
│   │  ┌───────▼───────┐  │   ipc.invoke    │  ┌───────▼───────┐  │      │
│   │  │ BatchManager  │──┼──────────────────┼──│ BatchHandler  │  │      │
│   │  └───────────────┘  │                  │  └───────────────┘  │      │
│   │                     │                  │                     │      │
│   │  ┌───────────────┐  │   MessagePort   │  ┌───────────────┐  │      │
│   │  │ StreamClient  │◄─┼──────────────────┼─►│ StreamServer  │  │      │
│   │  └───────────────┘  │                  │  └───────────────┘  │      │
│   └─────────────────────┘                  └─────────────────────┘      │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

0. Port / Sub-port Model (New)

TuffTransport is modeled as “transport core + ports”. A port is a logical channel layered on top of the same transport, used to isolate domains (storage, corebox, plugin, worker, etc.).

EXAMPLE.VUE
TuffTransport
  ├─ port('storage')     // config/subscription
  ├─ port('corebox')     // search/render
  └─ port('plugin:xxx')  // plugin isolation

Key points:

  • Ports only handle onMessage/onStream and routing rules, not the IPC implementation.
  • Actual transport (Main/Renderer/Plugin/Worker) is handled by the impl layer; ports provide a unified protocol surface.
  • Ports have independent lifecycles, making cleanup on module unload reliable.

1. Event System Design

Why Not Strings?

The legacy Channel API used string-based event names:

EXAMPLE.TS
// Problems with string-based events:
channel.send('core-box:serch:query', data)  // Typo: "serch" - no error!
channel.send('core-box:search:query', { txt: 'hi' })  // Wrong field - no error!

Issues:

  1. No autocomplete - Must remember exact event names
  2. No type checking - Payload types unknown at compile time
  3. Refactoring risk - Renaming events requires manual find/replace
  4. Runtime errors - Typos only discovered at runtime

TuffEvent Solution

TuffEvent uses TypeScript's type system to enforce correctness:

EXAMPLE.TS
// TuffEvent definition (simplified)
interface TuffEvent<TRequest, TResponse, TNamespace, TModule, TAction> {
  readonly __brand: 'TuffEvent'  // Brand for runtime checking
  readonly namespace: TNamespace
  readonly module: TModule
  readonly action: TAction
  readonly _request: TRequest    // Phantom type for request
  readonly _response: TResponse  // Phantom type for response
  toString(): string
}

Key Design Decisions:

  1. Branded Type - __brand: 'TuffEvent' enables runtime type checking
  2. Phantom Types - _request and _response exist only at type level
  3. Immutable - Events are frozen with Object.freeze()
  4. String Conversion - toString() returns event name for IPC

Event Builder Pattern

The builder pattern ensures events are constructed correctly:

EXAMPLE.TS
defineEvent('namespace')     // Returns TuffEventBuilder<'namespace'>
  .module('module')          // Returns TuffModuleBuilder<'namespace', 'module'>
  .event('action')           // Returns TuffActionBuilder<'namespace', 'module', 'action'>
  .define<Req, Res>(opts)    // Returns TuffEvent<Req, Res, 'namespace', 'module', 'action'>

Why a Builder?

  • Enforces complete event definition
  • Provides clear, readable API
  • Enables IDE autocomplete at each step
  • Validates at compile time

2. Batch System Design

The Problem

Each IPC call has overhead (~1-5ms). Multiple sequential calls compound this:

EXAMPLE.TS
// Without batching: 3 IPC calls = 3-15ms overhead
const a = await channel.send('storage:get', { key: 'a' })  // IPC #1
const b = await channel.send('storage:get', { key: 'b' })  // IPC #2
const c = await channel.send('storage:get', { key: 'c' })  // IPC #3

Batch Flow

EXAMPLE.VUE
Request 1 ─┐
Request 2 ─┼─► BatchManager ─► [Window 50ms] ─► Single IPC
Request 3 ─┘       │                              │
                   │                              ▼
            windowMs timer              Main Process Handler
                   │                              │
                   ▼                              ▼
            Force flush if:              Process all requests
            - Timer expires                       │
            - Max size reached                    ▼
            - flush() called              Return all results
                                                  │
Response 1 ◄─┐                                    │
Response 2 ◄─┼─ Demultiplex ◄─────────────────────┘
Response 3 ◄─┘

BatchManager Implementation

EXAMPLE.TS
class BatchManager {
  private groups: Map<string, BatchGroup> = new Map()
  
  async add<TReq, TRes>(event: TuffEvent<TReq, TRes>, payload: TReq): Promise<TRes> {
    const config = event._batch
    
    // Skip batching if not enabled
    if (!config?.enabled) {
      return this.sendSingle(event, payload)
    }
    
    return new Promise((resolve, reject) => {
      const group = this.getOrCreateGroup(event)
      
      // Apply merge strategy
      this.applyStrategy(group, { payload, resolve, reject }, config)
      
      // Check flush conditions
      if (group.requests.length >= config.maxSize) {
        this.flush(event.toString())
      } else if (!group.timer) {
        group.timer = setTimeout(() => this.flush(event.toString()), config.windowMs)
      }
    })
  }
}

Merge Strategies

1. Queue (Default) All requests are kept and processed in order:

EXAMPLE.JSON
[{key:'a'}, {key:'b'}, {key:'a'}] → Process all 3

2. Dedupe Identical payloads share one request:

EXAMPLE.JSON
[{key:'a'}, {key:'b'}, {key:'a'}] → Process 2, both 'a' get same result

3. Latest Only the latest request per key is kept:

EXAMPLE.JSON
[{key:'a',v:1}, {key:'b'}, {key:'a',v:2}] → Process [{key:'a',v:2}, {key:'b'}]

3. Stream System Design

Why MessagePort?

Regular IPC has limitations for streaming:

  • Request-response pattern doesn't fit continuous data
  • Large payloads block the IPC channel
  • No backpressure handling

MessagePort Benefits:

  • Dedicated channel per stream
  • Non-blocking data transfer
  • Native backpressure support
  • Efficient for binary data

Stream Flow

EXAMPLE.VUE
Renderer                              Main Process
   │                                       │
   │─── 1. Request stream ──────────────►  │
   │    (via ipc.invoke)                   │
   │                                       │
   │◄── 2. Return { streamId, port2 } ─────│
   │    (port2 transferred)                │
   │                                       │
   │◄══ 3. Data chunks ════════════════════│
   │    (via MessagePort)                  │
   │                                       │
   │◄══ 4. More chunks... ═════════════════│
   │                                       │
   │◄══ 5. End signal ═════════════════════│
   │                                       │
   │─── 6. Port closed ───────────────────►│

StreamServer (Main Process)

EXAMPLE.TS
class StreamServer {
  async handleStreamRequest(eventName: string, payload: any, webContents: WebContents) {
    const { port1, port2 } = new MessageChannelMain()
    const streamId = generateId()
    
    // Send port2 to renderer
    webContents.postMessage('@tuff:stream:port', { streamId }, [port2])
    
    // Create context for handler
    const context: StreamContext = {
      emit: (chunk) => port1.postMessage({ type: 'data', chunk }),
      error: (err) => port1.postMessage({ type: 'error', message: err.message }),
      end: () => port1.postMessage({ type: 'end' }),
      isCancelled: () => this.cancelled.has(streamId)
    }
    
    // Execute handler
    await this.handlers.get(eventName)?.(payload, context)
    
    return { streamId }
  }
}

Backpressure Handling

When the consumer can't keep up:

EXAMPLE.TS
const config: StreamConfig = {
  enabled: true,
  bufferSize: 100,
  backpressure: 'buffer' // 'drop' | 'buffer' | 'error'
}
  • drop - New data discarded when buffer full
  • buffer - Data buffered (memory risk)
  • error - Error thrown when buffer full

4. Plugin Security

The Key Mechanism

Plugins run in isolated WebContentsView. To prevent unauthorized access:

EXAMPLE.VUE
┌─────────────────────────────────────────────────────────────────┐
│                    Plugin Security Flow                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. Plugin loads                                                 │
│     │                                                            │
│     ▼                                                            │
│  2. Main process generates unique key                            │
│     key = randomString() → stored in keyToNameMap                │
│     │                                                            │
│     ▼                                                            │
│  3. Key injected into plugin's preload                           │
│     window.$plugin.uniqueKey = key                               │
│     │                                                            │
│     ▼                                                            │
│  4. All plugin messages include key in header                    │
│     { header: { uniqueKey: key }, ... }                          │
│     │                                                            │
│     ▼                                                            │
│  5. Main process validates key                                   │
│     pluginName = keyToNameMap.get(key)                          │
│     if (!pluginName) reject()                                    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

PluginKeyManager

EXAMPLE.TS
interface PluginKeyManager {
  requestKey(pluginName: string): string   // Generate new key
  revokeKey(key: string): boolean          // Invalidate key
  resolveKey(key: string): string | undefined  // Get plugin name
  isValidKey(key: string): boolean         // Validate key
}

Security Context

Every handler receives security context:

EXAMPLE.TS
transport.on(SomeEvent, (payload, context) => {
  if (context.plugin) {
    console.log(`Request from plugin: ${context.plugin.name}`)
    console.log(`Key verified: ${context.plugin.verified}`)
  }
})

5. Error Handling

Error Flow

EXAMPLE.VUE
Renderer                              Main Process
   │                                       │
   │─── Request ──────────────────────────►│
   │                                       │
   │                              Handler throws error
   │                                       │
   │◄── TuffTransportError ────────────────│
   │    { code, message, eventName }       │
   │                                       │
   ▼
catch (err) {
  if (err instanceof TuffTransportError) {
    // Structured error handling
  }
}

Error Serialization

Errors are serialized for IPC:

EXAMPLE.TS
class TuffTransportError extends Error {
  toJSON() {
    return {
      name: 'TuffTransportError',
      code: this.code,
      message: this.message,
      eventName: this.eventName,
      timestamp: this.timestamp
    }
  }
  
  static fromJSON(obj) {
    return new TuffTransportError(obj.code, obj.message, {
      eventName: obj.eventName
    })
  }
}

6. Performance Considerations

IPC Overhead

OperationApproximate Time
Single IPC call1-5ms
Serialization (small)0.1ms
Serialization (large)1-10ms
MessagePort setup2-5ms
MessagePort message0.1-0.5ms

Optimization Strategies

  1. Batch by default - Enable batching for frequent events
  2. Stream for large data - Use MessagePort for >100KB
  3. Dedupe when possible - Share responses for identical requests
  4. Lazy evaluation - Only serialize when flushing batch

Memory Management

EXAMPLE.TS
// Cleanup patterns
onUnmounted(() => {
  // Cancel pending requests
  controller.cancel()
  
  // Remove handlers
  cleanup()
  
  // Flush batches
  transport.flush()
})

7. Comparison with Legacy Channel

AspectLegacy ChannelTuffTransport
Event DefinitionStringTuffEvent object
Type SafetyNoneFull TypeScript
AutocompleteNoneFull IDE support
BatchingManualAutomatic
StreamingNot supportedMessagePort
Error TypesGeneric ErrorTuffTransportError
Plugin SecurityuniqueKey headerPluginKeyManager
Backwards CompatN/AFull compatibility

Migration Path

EXAMPLE.TS
// Legacy code continues to work
channel.send('event', data)

// New code uses TuffTransport
transport.send(TuffEvent, data)

// They share the same IPC infrastructure

Best Practices

  • Use batching for high-frequency calls to reduce IPC overhead.
  • Stream large payloads to avoid blocking the main process.
  • Migrate core flows first, then cover edge capabilities.

Summary

TuffTransport provides:

  1. Type Safety - Compile-time event validation via TuffEvent
  2. Performance - Automatic batching reduces IPC overhead
  3. Streaming - MessagePort for large/continuous data
  4. Security - Plugin isolation via key mechanism
  5. Ergonomics - Clean API with full IDE support
  6. Compatibility - Works alongside legacy Channel API
Was this helpful?