How `globalThis.__piSessions` Survives Next.js Hot-Reload in Pi Web
globalThis.__piSessions persists across Next.js hot-reloads because Pi Web stores active sessions in a JavaScript global object that survives module replacement while the Node process stays alive.
Pi Web is an open-source framework that maintains AgentSessionWrapper instances across development server refreshes. The implementation relies on a simple but effective pattern: attaching session state to globalThis rather than module-level variables.
Why Module-Level State Disappears on Hot-Reload
Next.js hot-reload replaces a module's code without restarting the entire Node process. Module-level variables get reinitialized, but the global object remains intact. This behavior creates a challenge for maintaining long-lived connections like agent sessions.
In lib/rpc-manager.ts, Pi Web solves this by moving session storage from module scope to the global object.
The Global Registry Implementation
Declaring the Global Variable
The type declaration establishes TypeScript support for the custom global property:
// lib/rpc-manager.ts#L1371
declare global {
var __piSessions: Map<string, AgentSessionWrapper> | undefined;
}
This declaration allows the codebase to reference globalThis.__piSessions with proper typing.
Lazy Initialization Pattern
The registry initializes only on first access:
// lib/rpc-manager.ts#L1378-L1380
if (!globalThis.__piSessions) {
globalThis.__piSessions = new Map();
// ... registration logic
}
Critical behavior: On subsequent module loads after hot-reload, globalThis.__piSessions already exists. The condition evaluates to false, and the existing Map is reused.
Centralized Access Through getRegistry()
All session operations route through a single helper:
// lib/rpc-manager.ts#L1377-L1386
function getRegistry(): Map<string, AgentSessionWrapper> {
if (!globalThis.__piSessions) {
globalThis.__piSessions = new Map();
// shutdown hook registration ...
}
return globalThis.__piSessions;
}
This guarantees every caller receives the same global map reference.
Process Lifecycle vs. Hot-Reload
Pi Web registers cleanup handlers for actual process termination:
// lib/rpc-manager.ts#L1380-L1384
const cleanup = () => globalThis.__piSessions?.forEach((s) => s.destroy());
process.once("exit", cleanup);
// additional signals: SIGINT, SIGTERM, SIGHUP
Key distinction: These hooks fire when the Node process exits—not during hot-reload. The sessions remain alive and accessible to freshly loaded code.
Practical Usage Examples
Retrieving an Existing Session
After hot-reload, previously created sessions remain accessible:
import { getRpcSession } from '@/lib/rpc-manager';
const session = getRpcSession('session-id');
if (session?.isAlive()) {
// Same AgentSessionWrapper instance from before reload
await session.inner.prompt({ message: 'Ask again after reload' });
}
Creating New Sessions
New sessions automatically join the persistent registry:
import { startRpcSession } from '@/lib/rpc-manager';
const wrapper = await startRpcSession({ cwd: '/home/user/project' });
console.log('New session id:', wrapper.sessionId);
// Stored in globalThis.__piSessions for future access
Source File Architecture
| File | Responsibility |
|---|---|
lib/rpc-manager.ts |
Global session registry, getRegistry(), getRpcSession(), startRpcSession() |
lib/agent-client.ts |
Typed fetch client for /api/agent endpoints |
app/api/agent/[id]/route.ts |
API route forwarding HTTP to AgentSessionWrapper |
Summary
globalThis.__piSessionsstores session state outside module scope- Lazy initialization creates the Map once, reuses it thereafter
- Hot-reload replaces code but preserves the global object
- Process exit hooks clean up sessions only on actual server shutdown
getRegistry()centralizes access to guarantee consistent references
Frequently Asked Questions
Does this pattern work with Next.js App Router?
Yes. The globalThis object is shared across all server-side code in the same Node process. Whether using Pages Router or App Router, the registry persists as long as the development server keeps running.
What happens to sessions on actual server restart?
Sessions are destroyed. The process.once("exit", cleanup) handler and related signal listeners iterate through all entries in globalThis.__piSessions and call s.destroy() on each AgentSessionWrapper before the process terminates.
Could this cause memory leaks during development?
Unlikely in practice. The registry only grows when new sessions are explicitly created via startRpcSession(). Each session occupies memory, but typical development workflows create sessions in response to user actions. The Map structure itself has minimal overhead.
Is this pattern specific to Pi Web or widely applicable?
The globalThis persistence pattern applies to any Node.js application with hot-reload requirements. Popular frameworks like Next.js, Nuxt, and Vite all preserve globalThis across module replacement. Pi Web's implementation in lib/rpc-manager.ts demonstrates a clean, production-ready application of this technique.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →