0.1.0 • Published 5 months ago

@gitorial/sync v0.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

@gitorial/sync

Version Status License

A TypeScript/JavaScript library for real-time tutorial state synchronization between educational websites and VS Code extensions. This package provides both client and server components enabling platforms like DotCodeSchool to seamlessly connect with VS Code extensions for synchronized tutorial experiences.

🎯 Problem Solved

The Challenge: Educational websites cannot directly communicate with VS Code extensions running on localhost due to browser security restrictions (CORS, mixed content policies).

The Solution: A relay server with sophisticated session orchestration acts as a bridge, enabling secure, real-time synchronization between:

  • 🌐 Educational Websites (DotCodeSchool, CodeAcademy, etc.)
  • 🔧 VS Code Extensions (tutorial guides, code navigation)

🏗️ Architecture Overview

Educational Website  ←→  RelaySessionOrchestrator  ←→  VS Code Extension
   (RelayClient)              (WebSocket)               (RelayClient)

Key Components:

  • RelaySessionOrchestrator: Manages sessions with sophisticated role coordination
  • RelayClient: Universal client for both websites and VS Code extensions
  • Sync Phase Management: Explicit state machine for connection and control phases
  • Dynamic Role Assignment: ACTIVE/PASSIVE roles with seamless control transfer
  • Session Lifecycle: Automatic cleanup, health monitoring, and reconnection

✨ Key Features

  • 🔄 Sync Phase Management: Explicit DISCONNECTED → CONNECTED_IDLE → ACTIVE/PASSIVE phases
  • 👑 Role Coordination: Automatic role assignment with conflict resolution
  • 🔄 Control Transfer: Seamless handoff between website and extension
  • 🛡️ Session Security: Temporary, user-controlled session tokens
  • 📱 Real-time Sync: WebSocket-based tutorial state synchronization
  • 🔧 Self-Hostable: Deploy your own relay server
  • 🌐 Universal Compatibility: Works in Node.js and browsers

🚀 Quick Start

For VS Code Extension Developers

import { RelayClient } from '@gitorial/sync';

const client = new RelayClient({
  baseUrl: 'http://localhost:8080',
  wsUrl: 'ws://localhost:8080',
  sessionEndpoint: '/api/sessions'
});

// Create session and connect
const session = await client.createSessionAndConnect({ 
  tutorial: 'react-basics' 
});

// Choose sync direction (become ACTIVE to receive state)
await client.pullStateFromPeer();

console.log(`Share session: ${session.id}`);

// Listen for tutorial updates from website
client.on('tutorialStateUpdated', (state) => {
  console.log(`Tutorial: ${state.tutorialTitle}`);
  console.log(`Step: ${state.stepContent.index + 1}/${state.totalSteps}`);
  // Update VS Code UI, navigate to files, etc.
});

// Send tutorial progress to website (only ACTIVE clients can send)
client.sendTutorialState({
  tutorialId: 'react-basics',
  tutorialTitle: 'React Fundamentals',
  totalSteps: 10,
  isShowingSolution: false,
  stepContent: {
    id: 'step-5',
    title: 'useState Hook',
    commitHash: 'abc123',
    type: 'action',
    index: 4
  }
});

For Educational Website Developers

import { RelayClient } from '@gitorial/sync';

const client = new RelayClient({
  baseUrl: 'http://localhost:8080',
  wsUrl: 'ws://localhost:8080',
  sessionEndpoint: '/api/sessions'
});

// Connect to existing session
const sessionId = new URLSearchParams(location.search).get('session');
await client.connectToSession(sessionId);

// Choose sync direction (become PASSIVE to send state)
await client.pushStateToPeer();

// Listen for VS Code extension updates
client.on('tutorialStateUpdated', (state) => {
  // Update website UI
  updateTutorialStep(state.stepContent.index);
});

// Send website progress to VS Code (PASSIVE clients send initial state)
client.on('syncPhaseChanged', (event) => {
  if (event.newPhase === 'passive') {
    // Now in PASSIVE phase, can send state updates
    client.sendTutorialState(getCurrentTutorialState());
  }
});

🔄 Sync Phase Management

The client uses an explicit state machine for connection and role management:

Phase Transitions

DISCONNECTED → CONNECTING → CONNECTED_IDLE
                                ↓
              ACTIVE ←→ PASSIVE (via control transfer)

Phase Descriptions

  • DISCONNECTED: No connection to relay server
  • CONNECTING: Establishing WebSocket connection
  • CONNECTED_IDLE: Connected but no sync direction chosen
  • ACTIVE: Has control, can send tutorial state and requests
  • PASSIVE: Receives updates, limited sending capabilities

Phase Methods

// Check current phase
const phase = client.getCurrentSyncPhase();
const isActive = client.isActive();
const isPassive = client.isPassive();
const isConnectedIdle = client.isConnectedIdle();

// Choose sync direction (only in CONNECTED_IDLE)
await client.pullStateFromPeer(); // Become ACTIVE
await client.pushStateToPeer();   // Become PASSIVE

// Control transfer (between ACTIVE ↔ PASSIVE)
client.offerControlToPeer();     // Offer control to peer
client.acceptControlTransfer();  // Accept control from peer
client.releaseControl();         // Release control (ACTIVE → PASSIVE)

🎛️ Control Flow Examples

DotCodeSchool + VS Code Extension Workflow

// 1. Extension creates session and becomes ACTIVE
const extension = new RelayClient(config);
const session = await extension.createSessionAndConnect();
await extension.pullStateFromPeer(); // ACTIVE phase

// 2. Website connects and becomes PASSIVE
const website = new RelayClient(config);
await website.connectToSession(session.id);
await website.pushStateToPeer(); // PASSIVE phase

// 3. Extension sends tutorial state (ACTIVE → PASSIVE)
extension.sendTutorialState(currentState);

// 4. Website receives state and updates UI
website.on('tutorialStateUpdated', updateUI);

// 5. Control transfer: Website takes control
website.acceptControlTransfer(); // PASSIVE → ACTIVE
// Extension automatically becomes PASSIVE

// 6. Website can now send state updates
website.sendTutorialState(newState);

🔧 Server Setup (RelaySessionOrchestrator)

import { RelaySessionOrchestrator } from '@gitorial/sync';
import express from 'express';
import { WebSocketServer } from 'ws';

const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });

// Create session orchestrator with modular architecture
const sessionOrchestrator = new RelaySessionOrchestrator({
  sessionTimeoutMs: 30 * 60 * 1000, // 30 minutes
  pingIntervalMs: 30 * 1000,        // 30 seconds
  cleanupIntervalMs: 60 * 1000,     // 1 minute
  enableRoleManagement: true,
  defaultConflictResolution: 'FIRST_COME_FIRST_SERVED'
});

// HTTP API for session management
app.post('/api/sessions', (req, res) => {
  const session = sessionOrchestrator.createSession(req.body);
  res.json(session);
});

app.get('/api/sessions/:sessionId', (req, res) => {
  const session = sessionOrchestrator.getSession(req.params.sessionId);
  res.json(session || { error: 'Session not found' });
});

// WebSocket upgrade handling
wss.on('connection', (socket, request) => {
  const url = new URL(request.url!, `http://${request.headers.host}`);
  const sessionId = url.searchParams.get('session');
  
  if (!sessionId) {
    socket.close(1008, 'Session ID required');
    return;
  }

  sessionOrchestrator.handleUpgrade(sessionId, socket, request);
});

// Start the orchestrator
sessionOrchestrator.start();
server.listen(8080);

📚 API Reference

RelayClient

interface RelayClientConfig {
  baseUrl: string;           // HTTP API base URL
  wsUrl: string;             // WebSocket URL
  sessionEndpoint: string;   // Session API endpoint
  connectionTimeout?: number;
  autoReconnect?: boolean;
  maxReconnectAttempts?: number;
  reconnectDelay?: number;
}

class RelayClient {
  // Session lifecycle
  async createSessionAndConnect(metadata?: any): Promise<SessionData>;
  async connectToSession(sessionId: string): Promise<void>;
  disconnect(): void;

  // Sync phase management
  getCurrentSyncPhase(): SyncPhase;
  isConnectedIdle(): boolean;
  isActive(): boolean;
  isPassive(): boolean;

  // Sync direction (only in CONNECTED_IDLE)
  async pullStateFromPeer(): Promise<void>;  // Become ACTIVE
  async pushStateToPeer(): Promise<void>;    // Become PASSIVE

  // Tutorial state (phase-dependent permissions)
  sendTutorialState(state: TutorialSyncState): void;  // ACTIVE only
  requestTutorialState(): void;                       // ACTIVE only

  // Control transfer
  offerControlToPeer(): void;
  acceptControlTransfer(): void;
  releaseControl(): void;

  // Status
  isConnected(): boolean;
  getCurrentSessionId(): string | null;
  getClientId(): string;
}

Events

// Sync phase changes
client.on('syncPhaseChanged', (event: SyncPhaseChangeEvent) => {
  console.log(`Phase: ${event.oldPhase} → ${event.newPhase}`);
});

// Tutorial state updates
client.on('tutorialStateUpdated', (state: TutorialSyncState) => {
  // Handle state updates
});

// Control events
client.on('controlRequested', (event) => {
  // Peer wants control
  client.acceptControlTransfer(); // or decline
});

client.on('controlOffered', (event) => {
  // Peer offers control
  client.acceptControlTransfer();
});

// Connection events
client.on('connected', () => console.log('Connected'));
client.on('disconnected', () => console.log('Disconnected'));
client.on('clientConnected', (clientId) => console.log('Peer connected'));
client.on('error', (error) => console.error('Error:', error));

Tutorial State Interface

interface TutorialSyncState {
  tutorialId: string;
  tutorialTitle: string;
  totalSteps: number;
  isShowingSolution: boolean;
  stepContent: {
    id: string;
    title: string;
    commitHash: string;
    type: 'section' | 'action' | 'template';
    index: number;
  };
  repoUrl?: string;
}

🛡️ Security & Privacy

  • Session Tokens: Random, temporary, user-controlled
  • No Data Persistence: Relay only forwards messages, doesn't store data
  • Self-Hostable: Run your own relay server for full control
  • Role-Based Access: Sync phases control who can send/receive state
  • Conflict Resolution: Built-in strategies for role conflicts
  • Session Lifecycle: Automatic cleanup and expiration

📦 Installation

For VS Code Extensions

npm install @gitorial/sync

For Node.js Applications

npm install @gitorial/sync

For Web Applications

npm install @gitorial/sync
# or use CDN

🔨 Development

# Install dependencies
pnpm install

# Build
pnpm run build

# Run tests
pnpm run test

# Development server
pnpm run dev

Package Structure

  • dist/index.js - CommonJS for Node.js/VS Code extensions
  • dist/index.esm.js - ES Modules
  • Type definitions included for full TypeScript support

🏛️ Architecture

For detailed information about the internal architecture, sync phases, and design patterns, see ARCHITECTURE.md.

📄 License

MIT License - see LICENSE for details.

🤝 Contributing

Contributions welcome! Please read our contributing guidelines and submit pull requests.


Need Help? Check out the Architecture Guide or open an issue.