Skip to content

Latest commit

 

History

History
488 lines (375 loc) · 10.6 KB

File metadata and controls

488 lines (375 loc) · 10.6 KB

ForgeUI Bridge System

Go-JavaScript Bridge for calling server-side Go functions from client-side JavaScript

Overview

The ForgeUI Bridge provides a seamless way to call Go functions from JavaScript without writing custom API endpoints. It supports:

  • HTTP (JSON-RPC 2.0): Single and batch requests
  • WebSocket: Real-time bidirectional communication
  • Server-Sent Events (SSE): Server-to-client streaming
  • Type Safety: Automatic parameter validation
  • Security: Built-in CSRF protection, rate limiting, and authentication
  • Alpine.js Integration: Magic helpers and directives

Quick Start

Server Side (Go)

package main

import (
	"github.com/xraph/forgeui/bridge"
	"github.com/xraph/forgeui/router"
)

func main() {
	// Create bridge
	b := bridge.New()

	// Register functions
	b.Register("greet", func(ctx bridge.Context, input struct {
		Name string `json:"name"`
	}) (struct {
		Message string `json:"message"`
	}, error) {
		return struct {
			Message string `json:"message"`
		}{Message: "Hello, " + input.Name}, nil
	})

	// Create router and enable bridge
	r := router.New()
	bridge.EnableBridge(r, b)

	// Start server
	http.ListenAndServe(":8080", r)
}

Client Side (JavaScript)

<!DOCTYPE html>
<html>
<head>
	<script src="https://unpkg.com/alpinejs@3" defer></script>
</head>
<body>
	<!-- Include bridge scripts -->
	<!-- Generated by bridge.BridgeScripts() -->

	<div x-data="{ name: '', greeting: '' }">
		<input x-model="name" placeholder="Your name">
		<button @click="greeting = await $bridge.call('greet', { name })">
			Greet
		</button>
		<p x-text="greeting"></p>
	</div>
</body>
</html>

Features

1. Function Registration

Register any Go function that matches the signature:

func(ctx bridge.Context, input InputType) (OutputType, error)

Example:

type CreateTodoInput struct {
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

type Todo struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

b.Register("createTodo", func(ctx bridge.Context, input CreateTodoInput) (*Todo, error) {
	// Create todo in database
	todo := &Todo{
		ID:    generateID(),
		Title: input.Title,
		Done:  input.Done,
	}
	
	return todo, nil
})

2. Function Options

Customize function behavior with options:

b.Register("adminAction", handler,
	bridge.RequireAuth(),                    // Require authentication
	bridge.RequireRoles("admin"),            // Require specific roles
	bridge.WithTimeout(10*time.Second),      // Custom timeout
	bridge.WithRateLimit(10),                // Rate limit (requests/min)
	bridge.WithCache(5*time.Minute),         // Cache results
	bridge.WithDescription("Admin action"),  // Documentation
)

3. Authentication & Authorization

// Require authentication
b.Register("protected", handler, bridge.RequireAuth())

// Require specific roles
b.Register("adminOnly", handler, 
	bridge.RequireAuth(),
	bridge.RequireRoles("admin", "superuser"),
)

// Access user in function
func handler(ctx bridge.Context, input InputType) (OutputType, error) {
	user := ctx.User()
	if user == nil {
		return nil, bridge.ErrUnauthorized
	}
	
	// Use user information
	userID := user.ID()
	// ...
}

4. JavaScript Client

Basic Usage

const bridge = new ForgeBridge({
	endpoint: '/api/bridge',
	timeout: 30000,
	csrf: document.cookie.match(/csrf_token=([^;]+)/)?.[1]
});

// Single call
const result = await bridge.call('functionName', { param: 'value' });

// Batch call
const results = await bridge.callBatch([
	{ method: 'func1', params: { a: 1 } },
	{ method: 'func2', params: { b: 2 } }
]);

// Streaming
const cleanup = bridge.stream('longRunning', {},
	(data) => console.log('Progress:', data),
	(error) => console.error('Error:', error)
);

Alpine.js Integration

<!-- Magic helper -->
<button @click="result = await $bridge.call('functionName', { param: value })">
	Call Function
</button>

<!-- Directive -->
<button x-bridge.click="greet:{name}">
	Greet
</button>

<!-- Store -->
<div x-data>
	<button @click="$store.bridge.call('getData', {})">
		Load Data
	</button>
	<div x-show="$store.bridge.loading">Loading...</div>
	<div x-show="$store.bridge.error" x-text="$store.bridge.error"></div>
</div>

5. Security

CSRF Protection

// Enable CSRF (default: enabled)
b := bridge.New(bridge.WithCSRF(true))

// Generate token
token, _ := bridge.GenerateCSRFToken()

// Set cookie
bridge.SetCSRFCookie(w, token, "csrf_token")

Rate Limiting

// Global rate limit
b := bridge.New(bridge.WithDefaultRateLimit(60)) // 60 req/min

// Per-function rate limit
b.Register("func", handler, bridge.WithRateLimit(10)) // 10 req/min

Input Validation

// Automatic validation from struct tags
type Input struct {
	Email    string `json:"email" validate:"email"`
	Age      int    `json:"age" validate:"range:0,150"`
	Username string `json:"username" validate:"required"`
}

// Custom validators
if err := bridge.ValidateEmail(email); err != nil {
	return nil, err
}

if err := bridge.ValidateRange(age, 0, 150); err != nil {
	return nil, err
}

6. Transports

HTTP (JSON-RPC 2.0)

Default transport for single and batch requests.

// Automatically registered at:
// POST /api/bridge
// POST /api/bridge/batch

WebSocket

For real-time bidirectional communication.

// Automatically registered at:
// GET /api/bridge/ws

// Broadcast to all clients
wsHandler := bridge.NewWSHandler(b)
wsHandler.Broadcast(bridge.Event{
	Type: "notification",
	Data: "Hello everyone!",
})

// Send to specific user
wsHandler.SendToUser(userID, bridge.Event{
	Type: "message",
	Data: "Personal message",
})

Server-Sent Events (SSE)

For server-to-client streaming.

// Automatically registered at:
// GET /api/bridge/stream?method=funcName&params=...

7. Hooks

Execute code before/after function calls:

// Before call
b.GetHooks().Register(bridge.BeforeCall, func(ctx bridge.Context, data bridge.HookData) {
	log.Printf("Calling %s with params: %v", data.FunctionName, data.Params)
})

// After call
b.GetHooks().Register(bridge.AfterCall, func(ctx bridge.Context, data bridge.HookData) {
	log.Printf("Function %s took %dμs", data.FunctionName, data.Duration)
})

// On error
b.GetHooks().Register(bridge.OnError, func(ctx bridge.Context, data bridge.HookData) {
	log.Printf("Error in %s: %v", data.FunctionName, data.Error)
})

// On success
b.GetHooks().Register(bridge.OnSuccess, func(ctx bridge.Context, data bridge.HookData) {
	log.Printf("Function %s succeeded: %v", data.FunctionName, data.Result)
})

8. Caching

// Enable caching
b := bridge.New(bridge.WithCache(true))

// Cache specific function results
b.Register("expensiveOp", handler, 
	bridge.WithCache(10*time.Minute), // Cache for 10 minutes
)

9. Introspection

List all registered functions and their metadata:

// Automatically registered at:
// GET /api/bridge/functions

// Programmatic access
functions := b.ListFunctionInfo()
for _, fn := range functions {
	fmt.Printf("Function: %s\n", fn.Name)
	fmt.Printf("  Description: %s\n", fn.Description)
	fmt.Printf("  Requires Auth: %v\n", fn.RequireAuth)
	fmt.Printf("  Input Type: %s\n", fn.TypeInfo.InputType)
	fmt.Printf("  Output Type: %s\n", fn.TypeInfo.OutputType)
}

Configuration

Bridge Options

b := bridge.New(
	bridge.WithTimeout(30*time.Second),      // Default timeout
	bridge.WithMaxBatchSize(10),             // Max batch size
	bridge.WithCSRF(true),                   // Enable CSRF
	bridge.WithCORS(true),                   // Enable CORS
	bridge.WithAllowedOrigins("*"),          // Allowed origins
	bridge.WithDefaultRateLimit(60),         // Default rate limit
	bridge.WithCache(true),                  // Enable caching
)

Script Generation

// Inline scripts
html := bridge.BridgeScripts(bridge.ScriptConfig{
	Endpoint:      "/api/bridge",
	CSRFToken:     csrfToken,
	IncludeAlpine: true,
})

// External scripts
html := bridge.BridgeScriptsExternal(bridge.ScriptConfig{
	Endpoint:      "/api/bridge",
	CSRFToken:     csrfToken,
	IncludeAlpine: true,
})

Error Handling

Server Side

func handler(ctx bridge.Context, input Input) (Output, error) {
	if input.Invalid {
		return nil, bridge.NewError(bridge.ErrCodeBadRequest, "Invalid input")
	}
	
	// Or use predefined errors
	if !authorized {
		return nil, bridge.ErrUnauthorized
	}
	
	// Custom error with details
	return nil, bridge.NewError(bridge.ErrCodeBadRequest, "Validation failed", map[string]any{
		"field": "email",
		"reason": "invalid format",
	})
}

Client Side

try {
	const result = await bridge.call('functionName', params);
} catch (err) {
	if (err instanceof BridgeError) {
		console.log('Error code:', err.code);
		console.log('Error message:', err.message);
		console.log('Error data:', err.data);
	}
}

Testing

Unit Tests

func TestMyFunction(t *testing.T) {
	b := bridge.New()
	
	b.Register("myFunc", myHandler)
	
	req := httptest.NewRequest("GET", "/", nil)
	ctx := bridge.NewContext(req)
	
	result, err := b.Call(ctx, "myFunc", json.RawMessage(`{"input":"value"}`))
	if err != nil {
		t.Fatal(err)
	}
	
	// Assert result
}

Integration Tests

func TestBridgeIntegration(t *testing.T) {
	b := bridge.New(bridge.WithCSRF(false))
	b.Register("test", handler)
	
	handler := bridge.NewHTTPHandler(b)
	
	req := bridge.Request{
		JSONRPC: "2.0",
		ID:      "1",
		Method:  "test",
		Params:  json.RawMessage(`{}`),
	}
	
	body, _ := json.Marshal(req)
	httpReq := httptest.NewRequest("POST", "/api/bridge", bytes.NewReader(body))
	w := httptest.NewRecorder()
	
	handler.ServeHTTP(w, httpReq)
	
	// Assert response
}

Performance

  • Overhead: <5ms per request (excluding function execution)
  • Throughput: 1000+ req/s on modest hardware
  • Memory: Minimal footprint with connection pooling
  • JavaScript Bundle: <3KB minified + gzipped

Best Practices

  1. Use meaningful function names: createUser not func1
  2. Validate inputs: Always validate user inputs
  3. Set timeouts: Use appropriate timeouts for long-running functions
  4. Enable rate limiting: Protect against abuse
  5. Use authentication: Protect sensitive functions
  6. Cache expensive operations: Use caching for expensive computations
  7. Handle errors gracefully: Return descriptive error messages
  8. Document functions: Use WithDescription() for API documentation
  9. Test thoroughly: Write unit and integration tests
  10. Monitor performance: Use hooks for logging and monitoring

License

MIT License - See LICENSE file for details