@rootlodge/reality-server
v1.2.2
Published
Socketless Real-Time Infrastructure - Server components with mesh awareness and storage adapters
Maintainers
Readme
@rootlodge/reality-server
Server package for Reality - Socketless Real-Time Infrastructure
Stateless HTTP handlers with mesh-aware coordination.
Reality Does NOT Own Your Data
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ ❌ Reality does NOT store your payloads │
│ ❌ Reality does NOT require a database │
│ ❌ Reality is NOT a data layer │
│ │
│ ✅ Reality tracks change METADATA (version, hash) │
│ ✅ Reality propagates INVALIDATION signals │
│ ✅ Reality coordinates MESH of servers │
│ │
│ Your app stores data. Reality tells clients when to refetch. │
│ │
└─────────────────────────────────────────────────────────────────────────┘Installation
npm install @rootlodge/reality-serverQuick Start (No Database Required!)
import { RealityServer } from '@rootlodge/reality-server';
// That's it! No database needed.
const server = new RealityServer({
serverId: 'server-1',
});
// When your data changes, invalidate the key
await server.invalidate('chat:room:123');
// Or invalidate multiple keys at once
await server.invalidateMany(['user:42', 'feed:global']);
// Mount the handler on your framework
const handler = server.getFetchHandler();
app.post('/reality/sync', handler);Execution Modes
Reality supports three execution modes:
┌─────────────────────────────────────────────────────────────────────────┐
│ EXECUTION MODES │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ SERVER-EXTERNAL (Default) SSR-EMBEDDED │
│ ──────────────────────── ──────────── │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Reality Server │ │ Your SSR App │ │
│ │ ┌────────────┐ │ │ ┌────────────┐ │ │
│ │ │ HTTP │ │◄── Requests │ │ Embedded │ │ │
│ │ │ Handler │ │ │ │ Reality │ │ ◄── In-process │
│ │ └────────────┘ │ │ └────────────┘ │ │
│ └────────────────┘ └────────────────┘ │
│ │
│ Use for: Production, scaling Use for: TanStack, Vite, Next.js │
│ │
└─────────────────────────────────────────────────────────────────────────┘Embedded Server for SSR
import { createEmbeddedRealityServer } from '@rootlodge/reality-server';
// Create embedded server for SSR
const embedded = createEmbeddedRealityServer({
serverId: 'ssr-server',
});
// Handle sync requests directly (no HTTP)
const response = await embedded.handleSync(request);
// Invalidate when data changes
await embedded.invalidate(['chat:room:123']);Optional Storage Adapters
Storage is OPTIONAL. Use it only if you need to:
- Persist metadata across restarts
- Share state between multiple server instances
Memory Storage (Default)
import { MemoryStorage } from '@rootlodge/reality-server';
const storage = new MemoryStorage();SQL Storage (PostgreSQL, MySQL, SQLite)
import { SQLStorage } from '@rootlodge/reality-server';
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const storage = new SQLStorage({
query: (sql, params) => pool.query(sql, params).then(r => r.rows),
dialect: 'postgres', // 'postgres' | 'mysql' | 'sqlite'
});Drizzle Adapter
import { createDrizzleAdapter } from '@rootlodge/reality-server';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
const client = postgres(process.env.DATABASE_URL);
const db = drizzle(client);
const storage = createDrizzleAdapter(db, {
tableName: 'reality_nodes',
});Prisma Adapter
import { createPrismaAdapter } from '@rootlodge/reality-server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const storage = createPrismaAdapter(prisma, {
modelName: 'RealityNode',
});DynamoDB Adapter
import { DynamoDBStorage } from '@rootlodge/reality-server';
import { DynamoDB } from '@aws-sdk/client-dynamodb';
const dynamodb = new DynamoDB({ region: 'us-east-1' });
const storage = new DynamoDBStorage({
client: dynamodb,
tableName: 'reality-nodes',
});Server Options
const server = new RealityServer({
// Required
storage: Storage, // Storage adapter instance
// Optional
serverId: string, // Unique server ID (auto-generated if not provided)
meshPeers: string[], // URLs of other servers for mesh coordination
redis: { // Optional Redis for pub/sub acceleration
publisher: RedisClient,
subscriber: RedisClient,
channel: 'reality:updates',
},
onError: (error) => void, // Error handler
});Updating Nodes
When your application data changes, update the corresponding Reality node:
// After adding a message
room.messages.push(newMessage);
// Update the Reality node with a hash of the current state
const hash = createHash(JSON.stringify(room.messages.map(m => m.id)));
await server.updateNode('chat:room:general', hash);
// Clients subscribed to 'chat:room:general' will detect the change on next syncHash Strategy
The hash should change whenever the underlying data changes. Options:
// Option 1: Hash of IDs (fast, detects additions/deletions)
const hash = createHash(items.map(i => i.id).join(','));
// Option 2: Hash of content (detects all changes)
const hash = createHash(JSON.stringify(items));
// Option 3: Timestamp (simple, always changes)
const hash = String(Date.now());
// Option 4: Version counter (from database)
const hash = String(record.version);HTTP Protocol
Request
POST /reality/sync HTTP/1.1
Content-Type: application/json
{
"known": {
"chat:room:general": 5,
"user:profile:123": 2
}
}Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"changed": {
"chat:room:general": {
"key": "chat:room:general",
"version": 6,
"hash": "abc123",
"updatedAt": 1699876543210
}
},
"mesh": {
"peers": ["server-2", "server-3"],
"serverVersion": 1
}
}Mesh Coordination
For multi-server deployments, servers gossip peer information:
const server1 = new RealityServer({
serverId: 'server-1',
peers: ['http://server-2:3000', 'http://server-3:3000'],
});
const server2 = new RealityServer({
serverId: 'server-2',
peers: ['http://server-1:3000', 'http://server-3:3000'],
});Mesh info is piggybacked on normal requests - no additional connections needed.
Optional Invalidation Adapters
Automatically invalidate Reality keys when your database changes:
Drizzle Auto-Invalidation
import {
RealityServer,
createDrizzleInvalidationAdapter
} from '@rootlodge/reality-server';
const reality = new RealityServer({ serverId: 'server-1' });
const adapter = createDrizzleInvalidationAdapter({
db,
keyExtractor: (table, operation, data) => {
if (table === 'messages') {
return [`chat:room:${data.roomId}`];
}
return [];
},
});
reality.setInvalidationAdapter(adapter);Prisma Auto-Invalidation
import { createPrismaInvalidationAdapter } from '@rootlodge/reality-server';
const adapter = createPrismaInvalidationAdapter({
prisma,
keyExtractor: (model, operation, data) => {
if (model === 'Message') {
return [`chat:room:${data.roomId}`];
}
return [];
},
});
reality.setInvalidationAdapter(adapter);Callback Adapter
import { createCallbackInvalidationAdapter } from '@rootlodge/reality-server';
const adapter = createCallbackInvalidationAdapter({
onInvalidate: async (keys) => {
console.log('Invalidated:', keys);
// Custom logic: notify external systems, etc.
},
});Redis Acceleration (Optional)
For faster cross-server propagation:
import Redis from 'ioredis';
const publisher = new Redis(process.env.REDIS_URL);
const subscriber = new Redis(process.env.REDIS_URL);
const server = new RealityServer({
storage,
serverId: 'server-1',
redis: {
publisher,
subscriber,
channel: 'reality:updates',
},
});Important: Redis is for acceleration only. The system MUST work correctly without it.
Framework Adapters
Fetch API (Bun, Deno, Workers)
import { createFetchHandler } from '@rootlodge/reality-server';
const handler = createFetchHandler(server);
// Bun
Bun.serve({
fetch(req) {
if (new URL(req.url).pathname === '/reality/sync') {
return handler(req);
}
return new Response('Not Found', { status: 404 });
},
});Express
import { createExpressAdapter } from '@rootlodge/reality-server';
import express from 'express';
const app = express();
app.use(express.json());
app.post('/reality/sync', createExpressAdapter(server));Fastify
import { createFetchHandler } from '@rootlodge/reality-server';
fastify.post('/reality/sync', async (request, reply) => {
const response = await createFetchHandler(server)(
new Request('http://localhost/reality/sync', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(request.body),
})
);
reply.status(response.status).send(await response.json());
});SSE Compatibility
For gradual migration from SSE:
// Server-side SSE compatibility endpoint
app.get('/events', async (req, res) => {
const response = await server.handleSSECompat(req, {
keys: ['notifications:all'],
heartbeatInterval: 30000,
});
// Stream response to client
});API Reference
RealityServer
class RealityServer {
constructor(options: RealityServerOptions);
// Update a node's version
updateNode(key: string, hash: string): Promise<void>;
// Get node metadata
getNode(key: string): Promise<RealityNodeMeta | null>;
// Handle sync request
handleSync(request: SyncRequest): Promise<SyncResponse>;
// Get mesh peers
getMeshPeers(): Promise<string[]>;
// Get server stats
getStats(): Promise<ServerStats>;
// SSE compatibility handler
handleSSECompat(request: Request, options: SSEOptions): Promise<Response>;
}Storage Interface
All storage adapters implement:
interface RealityStorage {
getNode(key: string): Promise<RealityNodeMeta | null>;
setNode(key: string, meta: RealityNodeMeta): Promise<void>;
incrementVersion(key: string): Promise<number>;
listChangedSince(version: number): Promise<RealityNodeMeta[]>;
getNodes(keys: string[]): Promise<Map<string, RealityNodeMeta>>;
getMaxVersion(): Promise<number>;
deleteNode(key: string): Promise<void>;
isHealthy(): Promise<boolean>;
}License
MIT
