Esiur for TypeScript
The TypeScript implementation of Esiur v3 brings real-time properties,
asynchronous function invocation, and events to browser and Node.js clients,
over the same EP protocol spoken by
Esiur for .NET — wire-compatible, with no
translation layer. It also includes Node.js server support for hosting your
own resources, through the optional ws package.
Install
npm install esiurFor Node.js 18–20 clients, or any Node.js server, install the optional WebSocket peer dependency:
npm install esiur wsNode.js 21+ and browsers can use the native WebSocket implementation for clients. EpServer is Node-only and always uses ws.
Connect to a remote resource
import { Warehouse } from "esiur";
const resource = await Warehouse.default.get("ep://127.0.0.1:10901/sys/counter");
console.log(resource.count);
resource.on("changed", () => console.log("updated:", resource.count));
await resource.increment();
Warehouse.get accepts local paths and EP URLs. ep://
is transported as ws://; eps:// as wss://.
A bare EP URL with no path returns just the connection:
const connection = await Warehouse.default.get("ep://127.0.0.1:10901");
A path can be resolved without knowing the remote type ahead of time — the
server answers a TypeDef query on demand and the client builds a
dynamic proxy (EpResource) from it. If you already have a
decorated resource class or TypeDef for the remote type, passing
it skips that round trip and gives you a typed reference instead:
const resource = await Warehouse.default.get(
"ep://127.0.0.1:10901/sys/counter",
Counter,
);Host resources
Define a resource by extending Resource and exporting
properties, events, and functions; put it in a Warehouse backed
by a store; serve the warehouse with EpServer:
import { Resource, Export, Warehouse, MemoryStore, EpServer, t } from "esiur";
class Greeter extends Resource {
@Export(t.i32) accessor visits = 0;
@Export(t.string, [t.string])
greet(name: string): string {
this.visits++;
return `Hi ${name}`;
}
}
const warehouse = new Warehouse();
await warehouse.put("sys", new MemoryStore());
await warehouse.put("sys/greeter", new Greeter());
await warehouse.open();
const server = await EpServer.listen({ port: 10901, warehouse });
Any client — TypeScript, JavaScript, or a .NET Esiur client — can then attach
to ep://host:10901/sys/greeter, read/write visits,
call greet(), and receive property-change notifications, all
without any code generation step.
Events
import { Resource, Export, event, type EventSource } from "esiur";
class Counter extends Resource {
@Export(t.i32) accessor count = 0;
@Export(t.string) changed: EventSource<string> = event<string>();
@Export(t.i32)
increment(): number {
this.count++;
this.changed.emit(`now ${this.count}`);
return this.count;
}
}Federation and relay
A resource fetched from one node can be handed to another node's own
Warehouse and re-served from there — attaching, property access,
function calls, and events all forward transparently through the relaying node:
// Node B fetches a resource from node A, without a local TypeDef.
const fromA = await connectionToA.get("sys/counter");
const raw = connectionToA.getAttachedResource(fromA.instanceId)!;
// Put it into B's own warehouse — it's now a first-class member there too.
await warehouseB.put("sys/relay", raw);
A third node connecting only to B can now attach sys/relay and
use it exactly as if it were local to B, including calls and events that
round-trip back through B to the original resource on A.
Resource lifecycle, static calls, and streaming
Beyond property/function/event access, a server built with
EpServer answers the full EP resource-management surface:
// Create a resource of a registered type, with initial property values.
const id = await client.createResource("sys/counter", typeDefId, new Map([[0, 42]]));
// Static/class-level calls — no resource instance required.
const sum = await client.staticCall(typeDefId, addFunctionIndex, 3, 4);
// Rename, detach (stop this connection's subscription), or delete.
await client.moveResource(id, "renamed");
await client.detach(id);
await client.deleteResource(id);Exported functions can also be async generators, streamed back to the caller chunk by chunk and pausable mid-stream:
import { StreamMode } from "esiur";
class Counter extends Resource {
@Export(t.i32, [t.i32], { streamMode: StreamMode.Pull, pausable: true })
async *countTo(n: number): AsyncGenerator<number> {
for (let i = 1; i <= n; i++) yield i;
}
}
The client calls it with invokeStream(streamMode, instanceId, index, ...args),
which returns an AsyncStreamReply — an async iterable, so you can
consume it with for await, or drive it manually with
.pull() / .halt() / .resume() / .terminate():
for await (const n of client.invokeStream(StreamMode.Pull, instanceId, countToIndex, 5)) {
console.log(n);
}Authentication
import {
AuthenticationMode,
EpConnectionContext,
Warehouse,
} from "esiur";
const warehouse = new Warehouse();
warehouse.registerAuthenticationProvider(new MyPasswordProvider());
const connection = await warehouse.get(
"ep://localhost:10901",
new EpConnectionContext({
authenticationMode: AuthenticationMode.InitializerIdentity,
identity: "alice",
authenticationProtocol: "password-sha3-v1",
domain: "test",
}),
);Servers register password providers on the served warehouse the same way:
warehouse.registerAuthenticationProvider(new MyPasswordProvider());
EpConnectionContext and Warehouse's auth methods
also accept the PascalCase field/method names used by Esiur for .NET
(RegisterAuthenticationProvider,
new EpConnectionContext({ AuthenticationMode, Identity, Domain }))
as aliases, for convenience when porting code between the two platforms.
React bindings
Optional hooks, importable from esiur/react — has no effect on
the core bundle, since react is only touched if you import this
entry point:
import { useProperty, useResource, useResourceEvent } from "esiur/react";
function CounterView({ counter }: { counter: unknown }) {
const count = useProperty<number>(counter, "count");
return <div>{count}</div>;
}CLI
esiur installs a bin/esiur command that forwards to
Esiur.CLI, a .NET global tool used for tasks like generating
typed client stubs from resource definitions. If the tool isn't installed
yet, running npx esiur prints install instructions
(dotnet tool install -g Esiur.CLI); otherwise it forwards
argv/stdio straight through and propagates the exit code.
Build
npm install
npm run check
npm run build
npm run check runs type checking, ESLint, and the test suite.
The runtime package supports Node.js 18+. The dev toolchain requires
Node.js 18.18+.
Status & known gaps
The v3 TypeScript port implements the full EP wire protocol: attach/reattach
(including reconnect resubscription), property read/write with change
notifications, function invocation, ref-counted event subscription
(.on()/.off()), server-side TypeDef answering (so
clients can attach without a predeclared type), resource lifecycle
(create/delete/move/detach), static calls, streaming function replies
(pull/push, pausable, terminable), AES transport encryption, and the
permissions/rate-limiting/auditing manager layer. EpResource
implements IResource, so a resource fetched from one node can be
relayed through another node's warehouse to a third.
[id, link] pairs instead); and direct raw TCP transport is
client-only — there's no raw-TCP server.