@lazarv/rsc
v0.0.0-experimental-04f2240-20260306-7fcde375
Published
Bundler-agnostic React Server Components serialization and deserialization
Maintainers
Readme
@lazarv/rsc
A bundler-agnostic, environment-agnostic React Server Components (RSC) serialization and deserialization library built on React's Flight protocol. Not a framework — a universal data transport layer.
This package provides a standalone implementation of the Flight protocol without any direct dependency on the react package and without bundler-specific mechanisms like Webpack's __webpack_require__. It is part of the @lazarv/react-server project.
Why
React's official react-server-dom-webpack package is tightly coupled to Webpack manifests and Node.js APIs. @lazarv/rsc removes both constraints:
- Bundler-agnostic — no Webpack plugin, no Vite plugin, no bundler manifests. Consumers wire up their own
moduleResolver/moduleLoaderinterfaces. - Environment-agnostic — built on Web Platform APIs (
ReadableStream,WritableStream,TextEncoder,FormData,Blob,URL, …). The same code runs in Node.js, Deno, Bun, Cloudflare Workers, the browser, or any runtime that supports the Web Platform. - No direct React imports — uses
Symbol.for()to access React internals, so it works with any compatible React version. - Full Flight protocol parity — Elements, Promises, Map, Set, Date, BigInt, RegExp, Symbol, URL, URLSearchParams, FormData, TypedArrays, ArrayBuffer, DataView, Blob, ReadableStream, async iterables, client/server references, Suspense, Fragment, lazy, memo, forwardRef, context, Activity, ViewTransition, and more.
How @lazarv/react-server uses it
@lazarv/react-server is a Vite-based React Server Components framework. It currently uses @lazarv/rsc for:
- Logger proxy — serializing structured log data across environment boundaries using the Flight protocol.
- Caching / cache providers — saving and restoring UI or data snapshots in any storage backend via RSC serialization.
With planned expansion to full cross-environment usage (worker threads, edge runtimes, cross-process communication) leveraging the environment-agnostic design of this package.
By extracting the Flight protocol into a standalone package, any tool or framework can adopt RSC serialization without buying into a specific bundler or runtime.
Use Cases
| Direction | Example | |---|---| | Server → Client | Streaming serialized React trees or structured data | | Client → Server | Sending action arguments, form data, console logs in RSC format | | Worker threads | Passing serialized React trees between threads | | Cache providers | Saving/restoring UI or data snapshots in any storage backend | | Cross-process | Piping RSC payloads between server processes | | Any ↔ Any | Browser ↔ Server ↔ Worker ↔ Edge ↔ Cache |
Installation
npm install @lazarv/rsc
# or
pnpm add @lazarv/rsc
# or
yarn add @lazarv/rscPeer dependency: react >=19.0.0 (or >=0.0.0-experimental).
Entry Points
Two universal entry points — the same code runs everywhere that supports Web Platform APIs:
| Entry | Purpose |
|---|---|
| @lazarv/rsc/server | Serialization — render, register references, decode replies |
| @lazarv/rsc/client | Deserialization — consume streams, encode replies, call actions |
There are no platform-specific sub-entries. No
/server.node,/server.edge,/client.browser, etc.
Usage
Server-side (Serialization)
import {
renderToReadableStream,
registerServerReference,
registerClientReference,
createClientModuleProxy,
createTemporaryReferenceSet,
decodeReply,
decodeAction,
decodeFormState,
decodeReplyFromAsyncIterable,
} from "@lazarv/rsc/server";Module Resolver
Provide a module resolver that tells the serializer how to resolve "use client" / "use server" references:
const stream = renderToReadableStream(element, {
moduleResolver: {
resolveClientReference(reference) {
return {
id: reference.$$id,
name: reference.$$name,
chunks: [/* chunk IDs to preload */],
};
},
resolveServerReference(reference) {
return {
id: reference.$$id,
name: reference.$$name,
};
},
},
onError(error) {
console.error("RSC Error:", error);
return error.digest; // returned as error.digest on the client
},
});Registering References
// Register a client component
const ClientComponent = registerClientReference(
{}, // proxy object
"./ClientComponent", // module ID
"default" // export name
);
// Register a server action
async function submitForm(formData) {
"use server";
}
registerServerReference(submitForm, "action:submitForm", "submitForm");
// Create a full module proxy (all named exports become client references)
const clientModule = createClientModuleProxy("./MyClientModule");Decoding Client Replies
// Decode a reply from the client (FormData or string body)
const args = await decodeReply(body, {
moduleLoader: {
loadServerAction: (id) => actionRegistry.get(id),
},
});
// Decode from a streaming async iterable
const args = await decodeReplyFromAsyncIterable(requestBodyStream, {
moduleLoader: {
loadServerAction: (id) => actionRegistry.get(id),
},
});
// Decode a form action (returns the action function)
const action = await decodeAction(formData, { moduleLoader });
// Decode form state for progressive enhancement
const state = await decodeFormState(actionResult, formData);Client-side (Deserialization)
import {
createFromReadableStream,
createFromFetch,
encodeReply,
createServerReference,
createTemporaryReferenceSet,
} from "@lazarv/rsc/client";Module Loader
Provide a module loader that tells the deserializer how to load client modules:
const moduleLoader = {
requireModule(metadata) {
// Synchronously return the module export
return moduleCache.get(metadata.id)?.[metadata.name];
},
preloadModule(metadata) {
// Optional: preload module chunks ahead of time
return import(metadata.id);
},
};Consuming Flight Streams
// From a ReadableStream — returns a synchronous thenable
// compatible with React's use() protocol
const result = createFromReadableStream(stream, {
moduleLoader,
callServer: async (id, args) => {
const response = await fetch("/action", {
method: "POST",
body: await encodeReply(args),
});
return createFromFetch(response, { moduleLoader, callServer });
},
});
// result.status === "pending" | "fulfilled" | "rejected"
// result.value is available synchronously once fulfilled
// From a fetch response
const result = createFromFetch(fetch("/rsc"), { moduleLoader, callServer });Server Action References
// Create a callable server action proxy
const myAction = createServerReference("action:myAction", callServer);
// Call it — args are encoded and sent via callServer
await myAction(arg1, arg2);
// .bind() works for partial application
const boundAction = myAction.bind(null, boundArg);
await boundAction(remainingArg);Encoding Replies
// Encode arguments for a server action call
// Returns string or FormData depending on content
const encoded = await encodeReply([arg1, arg2]);Temporary References
Temporary references allow non-serializable values (functions, React elements, class instances, local symbols) to survive a round-trip:
// Client-side: create a Map-based temp ref set
import { createTemporaryReferenceSet } from "@lazarv/rsc/client";
const tempRefs = createTemporaryReferenceSet();
// Encode with temp refs — non-serializable values are stored in the Map
const encoded = await encodeReply(args, { temporaryReferences: tempRefs });
// Later, recover values when consuming a response
const result = createFromReadableStream(responseStream, {
moduleLoader,
temporaryReferences: tempRefs,
});// Server-side: create a WeakMap-based temp ref set
import { createTemporaryReferenceSet } from "@lazarv/rsc/server";
const tempRefs = createTemporaryReferenceSet();
// Decode with temp refs
const args = await decodeReply(body, { temporaryReferences: tempRefs });
// Render with the same temp refs — proxies resolve back to $T references
const stream = renderToReadableStream(element, {
temporaryReferences: tempRefs,
});API Reference
Server API (@lazarv/rsc/server)
| Export | Description |
|---|---|
| renderToReadableStream(model, options?) | Serialize a React element tree to a Flight ReadableStream |
| decodeReply(body, options?) | Decode a FormData or string reply from the client |
| decodeReplyFromAsyncIterable(iterable, options?) | Decode a reply from a streaming AsyncIterable<Uint8Array> |
| decodeAction(body, options?) | Decode a server action invocation from FormData |
| decodeFormState(result, body) | Decode form state for progressive enhancement |
| registerServerReference(fn, id, name) | Register a function as a server reference ("use server") |
| registerClientReference(proxy, id, name) | Register an object as a client reference ("use client") |
| createClientModuleProxy(moduleId) | Create a Proxy where every property access returns a client reference |
| createTemporaryReferenceSet() | Create a WeakMap for temporary reference tracking |
| prerender(model, options?) | Prerender a model to a static prelude ReadableStream (waits for all async work) |
Client API (@lazarv/rsc/client)
| Export | Description |
|---|---|
| createFromReadableStream(stream, options?) | Deserialize a Flight stream into a React element tree (synchronous thenable) |
| createFromFetch(responsePromise, options?) | Deserialize a fetch() response into a React element tree |
| encodeReply(value, options?) | Encode a value for sending to the server (string or FormData) |
| createServerReference(id, callServer) | Create a callable proxy for a server action |
| createTemporaryReferenceSet() | Create a Map for temporary reference tracking |
Types
Full type definitions are in types.d.ts. Key interfaces:
interface ModuleResolver {
resolveClientReference?(reference: unknown): ClientReferenceMetadata | null;
resolveServerReference?(reference: unknown): ServerReferenceMetadata | null;
}
interface ModuleLoader {
preloadModule?(metadata: ClientReferenceMetadata): Promise<void> | void;
requireModule(metadata: ClientReferenceMetadata): unknown;
loadServerAction?(id: string): Promise<Function> | Function;
}
interface ClientReferenceMetadata {
id: string;
name: string;
chunks?: string[];
}
interface ServerReferenceMetadata {
id: string;
bound?: boolean;
}
interface RenderToReadableStreamOptions {
moduleResolver?: ModuleResolver;
onError?: (error: unknown) => string | void;
identifierPrefix?: string;
temporaryReferences?: Map<string, unknown>;
environmentName?: string;
filterStackFrame?: (sourceURL: string, functionName: string) => boolean;
signal?: AbortSignal;
}
interface CreateFromReadableStreamOptions {
moduleLoader?: ModuleLoader;
callServer?: (id: string, args: unknown[]) => Promise<unknown>;
temporaryReferences?: Map<string, unknown>;
typeRegistry?: Record<string, new (buffer: ArrayBuffer) => ArrayBufferView>;
}Serialization Coverage
All types supported by React's Flight protocol are implemented:
| Category | Types |
|---|---|
| Primitives | string, number, boolean, null, undefined, BigInt, Symbol (global) |
| Objects | Date, RegExp, URL, URLSearchParams, Map, Set, Error |
| Binary | ArrayBuffer, Int8Array, Uint8Array, Float32Array, DataView, Blob, … |
| Streams | ReadableStream, AsyncIterable |
| Form data | FormData |
| React | Elements, Fragments, Suspense, Lazy, Memo, ForwardRef, Context, Activity, ViewTransition |
| RSC | Client references ("use client"), Server references ("use server"), bound actions (.bind()) |
| Special | Promises, Thenables, Temporary references, Error digest propagation |
Design Decisions
Web standards only
This library targets the Web Platform API surface (ReadableStream, WritableStream, TextEncoder, FormData, Blob, URL, …). No Node.js-specific primitives — no stream.Readable, AsyncLocalStorage, or Buffer. A single entry point per side runs in any environment.
Abstract module loader — no bundler coupling
Only abstract moduleResolver / moduleLoader interfaces are supported. No Webpack plugin, no Vite plugin, no bundler-specific manifest generation. Consumers wire up their own resolution logic, which makes this library usable with any bundler or runtime.
No runtime-specific hooks
No Node.js ESM loader hooks, no CJS require hooks, no environment-specific registration. The consumer is responsible for handling "use client" / "use server" directive detection in their build tool or runtime.
Comparison with react-server-dom-webpack
| Capability | react-server-dom-webpack | @lazarv/rsc |
|---|---|---|
| Flight protocol | ✅ | ✅ Full parity |
| Bundler | Webpack only | Any (abstract interface) |
| Runtime | Node.js (+ browser client) | Any Web Platform runtime |
| renderToReadableStream | ✅ | ✅ |
| renderToPipeableStream | ✅ (Node.js) | — (use ReadableStream) |
| createFromReadableStream | ✅ | ✅ |
| createFromNodeStream | ✅ (Node.js) | — (use ReadableStream) |
| encodeReply / decodeReply | ✅ | ✅ |
| Temporary references | ✅ | ✅ |
| Bound actions (.bind()) | ✅ | ✅ |
| Error digest propagation | ✅ | ✅ |
| Synchronous thenable (use()) | ✅ | ✅ |
| Webpack plugin / manifest | ✅ | — (by design) |
| Node ESM loader hooks | ✅ | — (by design) |
| react-server condition gating | ✅ | — |
| Prerender | ✅ | ✅ |
Related
@lazarv/react-server— The Vite-based React Server Components framework that uses this package- react-server.dev — Documentation and guides
- React Flight protocol — The upstream React implementation
License
MIT
