Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ dist/
*.mjs
!tsconfig.json

# Rust
target/
Cargo.lock

# Lock files
pnpm-lock.yaml
package-lock.json

# IDE
.idea/
.vscode/
Expand Down
11 changes: 11 additions & 0 deletions examples/polyglot-coordination/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules/
dist/
*.js
*.d.ts
.DS_Store
.env
.env.local
__pycache__/
*.pyc
target/
Cargo.lock
181 changes: 181 additions & 0 deletions examples/polyglot-coordination/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Polyglot Coordination Example

This example demonstrates **iii-engine** seamlessly coordinating services across **Python, Node.js, and Rust** without requiring HTTP endpoints for all services.

**Key differentiator**: The Python service communicates via **stdin/stdout IPC** (not HTTP), showing that iii abstracts away transport mechanisms entirely.

## Architecture

```
iii-engine (ws://127.0.0.1:49134)
|
+------------------------------+------------------------------+
| | |
Node.js User Node.js Data Node.js Stripe
Service Requester Bridge
(users.*) (analytics.*) (stripe.*)
| | |
| spawns subprocess HTTP calls
| | |
| Python Analytics Rust Fake Stripe
| (stdin/stdout) (HTTP :4040)
| NO HTTP ENDPOINTS
```

## Business Scenario: SaaS User Onboarding

A realistic workflow tying all 3 languages together:

1. **New user signs up** → Node.js User Service
2. **Create Stripe customer + subscription** → Rust Fake Stripe (via HTTP, wrapped by iii)
3. **Run onboarding analytics** → Python Analytics (via stdin/stdout IPC)
4. **Store user profile with risk score** → Orchestrated by Node.js Workflow

## Prerequisites

- **Node.js 18+** or **Bun** (for workers)
- **Python 3.x** (stdlib only, no pip install needed)
- **Rust toolchain** (for Axum server)
- **iii-engine** running (`iii` command)

## Quick Start

### Terminal 1: Start iii-engine
```bash
iii
```

### Terminal 2: Start Rust Fake Stripe server
```bash
cd services/rust-stripe
cargo run --release
```

### Terminal 3: Install deps and start workers
```bash
pnpm install
pnpm dev
```

## Testing

### Full onboarding flow (coordinates all 3 languages)
```bash
curl -X POST http://localhost:3111/onboard \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "name": "Alice Smith", "plan": "pro"}'
```

Expected response:
```json
{
"message": "User onboarded successfully",
"traceId": "abc123...",
"user": {
"id": "usr_...",
"email": "[email protected]",
"name": "Alice Smith",
"plan": "pro",
"stripeCustomerId": "cus_...",
"subscriptionId": "sub_...",
"riskScore": 45.5
},
"stripeCustomer": { "id": "cus_...", "email": "...", "name": "..." },
"subscription": { "id": "sub_...", "status": "active", "plan": "pro" },
"analytics": { "riskScore": 45.5, "factors": ["custom_domain", "full_name_provided"] }
}
```

### Get metrics (calls Python analytics)
```bash
curl http://localhost:3111/onboard/metrics
```

### Onboard multiple users and check metrics
```bash
curl -X POST http://localhost:3111/onboard \
-d '{"email": "[email protected]", "name": "Bob", "plan": "free"}' \
-H "Content-Type: application/json"

curl -X POST http://localhost:3111/onboard \
-d '{"email": "[email protected]", "name": "Carol Chen", "plan": "enterprise"}' \
-H "Content-Type: application/json"

curl http://localhost:3111/onboard/metrics
```

## Key Insight

All function calls look identical regardless of transport:

```typescript
// Calls Rust via HTTP (wrapped by stripe-bridge)
await bridge.invokeFunction('stripe.createCustomer', { email, name })

// Calls Python via stdin/stdout IPC (wrapped by data-requester)
await bridge.invokeFunction('analytics.score', { userId, email, name })

// Calls local Node.js function
await bridge.invokeFunction('users.create', { email, name })
```

**The caller doesn't know or care about the underlying transport. That's the power of iii.**

## File Structure

```
polyglot-coordination/
├── README.md
├── package.json
├── tsconfig.json
├── services/
│ ├── python-analytics/
│ │ └── analytics.py # stdin/stdout JSON-RPC (NO HTTP)
│ └── rust-stripe/
│ ├── Cargo.toml
│ └── src/main.rs # Axum HTTP server
├── workers/
│ ├── user-service.ts # User CRUD + billing
│ ├── data-requester.ts # Spawns Python, exposes analytics.*
│ └── stripe-bridge.ts # Wraps Rust HTTP as iii functions
├── workflow/
│ └── onboarding.ts # Orchestrates full signup flow
└── lib/
├── bridge.ts # Shared bridge factory
├── python-ipc.ts # Python subprocess manager
└── types.ts # Shared TypeScript types
```

## How It Works

### Python Analytics (stdin/stdout)
The Python service has **no HTTP endpoints**. It reads JSON from stdin, processes it, and writes JSON to stdout:

```python
for line in sys.stdin:
request = json.loads(line)
result = handle_request(request['method'], request['params'])
sys.stdout.write(json.dumps({'id': request['id'], 'result': result}) + '\n')
```

The `data-requester.ts` worker spawns this script and communicates via stdin/stdout, then exposes the functionality as iii functions.

### Rust Fake Stripe (HTTP)
A standard Axum HTTP server providing a Stripe-like API. The `stripe-bridge.ts` worker makes HTTP calls to it and exposes the endpoints as iii functions.

### Node.js User Service
Pure Node.js with in-memory storage, directly registered as iii functions.

## Why This Matters

Traditional microservice orchestration requires:
- Every service to expose HTTP/gRPC endpoints
- Service discovery (Consul, etcd, K8s DNS)
- Load balancers, ingress controllers
- API gateways for routing

With iii-engine:
- Services can use **any transport** (HTTP, IPC, stdin/stdout, sockets)
- iii handles routing via **function names**
- No service discovery needed - iii is the registry
- No API gateway - iii exposes triggers directly
14 changes: 14 additions & 0 deletions examples/polyglot-coordination/lib/bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Bridge } from '@iii-dev/sdk'

const ENGINE_URL = process.env.III_ENGINE_URL ?? 'ws://127.0.0.1:49134'

export function createBridge(serviceName: string): Bridge {
return new Bridge(ENGINE_URL, {
otel: {
enabled: true,
serviceName,
metricsEnabled: true,
metricsExportIntervalMs: 5000,
},
})
}
128 changes: 128 additions & 0 deletions examples/polyglot-coordination/lib/python-ipc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { spawn, type ChildProcess } from 'child_process'
import { createInterface, type Interface } from 'readline'
import { EventEmitter } from 'events'

interface PendingRequest {
resolve: (result: unknown) => void
reject: (error: Error) => void
timeout: ReturnType<typeof setTimeout>
}

export class PythonIPC extends EventEmitter {
private process: ChildProcess | null = null
private readline: Interface | null = null
private requestId = 0
private pending = new Map<number, PendingRequest>()
private ready = false
private readyPromise: Promise<void>
private readyResolve!: () => void

constructor(
private scriptPath: string,
private timeoutMs = 30000,
private maxPendingRequests = 1000
) {
super()
this.readyPromise = new Promise((resolve) => {
this.readyResolve = resolve
})
}

async start(): Promise<void> {
this.process = spawn('python3', [this.scriptPath], {
stdio: ['pipe', 'pipe', 'pipe'],
})

this.readline = createInterface({
input: this.process.stdout!,
crlfDelay: Infinity,
})

this.readline.on('line', (line) => this.handleLine(line))

this.process.stderr?.on('data', (data) => {
const msg = data.toString().trim()
if (msg) console.error(`[Python stderr] ${msg}`)
})

this.process.on('close', (code) => {
this.ready = false
this.emit('close', code)
for (const [id, req] of this.pending) {
clearTimeout(req.timeout)
req.reject(new Error(`Python process exited with code ${code}`))
this.pending.delete(id)
}
})

this.process.on('error', (err) => {
this.emit('error', err)
})

await this.call('ping', {})
this.ready = true
this.readyResolve()
}

async waitReady(): Promise<void> {
return this.readyPromise
}

async call<T = unknown>(method: string, params: unknown): Promise<T> {
if (!this.process || !this.process.stdin) {
throw new Error('Python process not started')
}

if (this.pending.size >= this.maxPendingRequests) {
throw new Error(`Too many pending requests (max: ${this.maxPendingRequests})`)
}

const id = ++this.requestId
if (this.requestId > Number.MAX_SAFE_INTEGER - 1) {
this.requestId = 0
}
const request = JSON.stringify({ id, method, params })

return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pending.delete(id)
reject(new Error(`Request ${method} timed out after ${this.timeoutMs}ms`))
}, this.timeoutMs)

this.pending.set(id, { resolve: resolve as (r: unknown) => void, reject, timeout })
this.process!.stdin!.write(request + '\n')
})
}

private handleLine(line: string): void {
try {
const response = JSON.parse(line)
const { id, result, error } = response

const req = this.pending.get(id)
if (!req) return

clearTimeout(req.timeout)
this.pending.delete(id)

if (error) {
req.reject(new Error(error.message || error))
} else {
req.resolve(result)
}
} catch {
console.error(`[Python IPC] Failed to parse: ${line}`)
}
}

stop(): void {
if (this.process) {
this.process.kill()
this.process = null
}
if (this.readline) {
this.readline.close()
this.readline = null
}
}
}
Loading