The Aether Cloud Client for TypeScript is the official library for connecting to the Aether network. It provides a highly asynchronous, high-level API for secure peer-to-peer (P2P) messaging, access control management, and client-to-server interaction, all while abstracting the complexities of the low-level Aether binary protocol.
This library is designed for a modular architecture that seamlessly handles:
- Secure Multi-Step Handshake: Manages registration, key exchange, and the calculation of Proof-of-Work (PoW).
- End-to-End Encryption (E2EE): Handles all symmetric and asymmetric cryptography using pluggable providers (like Sodium).
- Connection and State Management: Maintains persistent connections to "Work Servers" and manages client identity, keys, and server lists (
ClientStateInMemory). - Intelligent Request Batching: Optimizes network traffic by consolidating multiple asynchronous requests (e.g., for Clouds, Server Descriptors, Access Groups) into single binary packets.
- RPC Conversion: Converts binary streams into concrete, type-safe Remote Procedure Call (RPC) interfaces for structured communication.
The Aether Cloud Client is installed via NPM or Yarn. Since the library is hosted on a private registry (http://nexus.aethernet.io/npm-private/), you must first configure your package manager to use this repository.
You must configure the Aether-specific NPM registry either globally or within your project's .npmrc file. Since the repository is accessible for reading without authentication, you only need to set the scope.
Option A: Project-Level Configuration (.npmrc)
Create a file named .npmrc in your project root with the following content:
@aethernet:registry=http://nexus.aethernet.io/npm-private/Option B: Global Configuration (using npm or yarn)
# Set the scope registry to the private nexus
npm config set @aethernet:registry http://nexus.aethernet.io/npm-private/Add the core client library, published under the @aethernet scope, and its required runtime dependencies.
# Add the core client library (assuming the name from package.json)
npm install @aethernet/aether-client
# Add required runtime dependencies, including those for crypto/networking
npm install isomorphic-ws bcryptjs crc-32 libsodium-wrappersThe client relies on libsodium-wrappers for cryptographic operations.
The library uses a modular approach for crypto providers. Before using the client, you must initialize the chosen provider (e.g., Sodium).
import { applySodium } from '@aethernet/client-ts';
// Must be called once before client instantiation
await applySodium(); All interactions with the Aether network are non-blocking and managed using Future objects.
Represents a void asynchronous operation (no return value).
| Method | Description |
|---|---|
AFuture.make() |
Creates a new pending Future. |
f.tryDone() |
Marks the Future as successfully completed. |
f.error(e) |
Marks the Future as completed with an error. |
f.to(runnable) |
Executes a runnable upon successful completion. |
AFuture.all(...f) |
Completes when all input Futures are finalized successfully. |
Represents an asynchronous operation that completes with a result of type T.
| Method | Description |
|---|---|
ARFuture.make<T>() |
Creates a new pending Future for result T. |
f.tryDone(value) |
Marks the Future as completed with result value. |
f.map(func) |
Transformation: Applies a synchronous function func(T) to transform the result type to E (ARFuture<E>). |
f.mapRFuture(func) |
Chaining: Chains the operation by applying an asynchronous function func(T) -> ARFuture<E> (similar to flatMap). |
f.to(consumer) |
Executes consumer(T) upon successful completion. |
f.toPromise(ms) |
Converts to a native JavaScript Promise<T>. |
Use mapRFuture to safely chain asynchronous operations, such as resolving a peer's location (Cloud) and then resolving the specific server details (ServerDescriptor) from that location.
client.getCloud(peerUid)
// Chain step 1: Get the Cloud (ARFuture<Cloud>)
.mapRFuture((cloud: Cloud) => {
// Chain step 2: Use the Cloud data to fetch the server descriptor
if (!cloud.data || cloud.data.length === 0) {
return ARFuture.ofThrow(new Error("Cloud is empty"));
}
return client.getServer(cloud.data[0]);
})
.to((server: ServerDescriptor | null) => {
console.log(`Peer server descriptor resolved: ${server?.id}`);
})
.onError(e => console.error("Chain failed:", e));The MessageNode is the crucial object that manages the connection routing logic, outgoing message queue, and incoming data stream for a specific peer (consumerUUID).
You obtain a MessageNode from the main client, which handles the necessary peer discovery (resolving the peer's Cloud and ServerDescriptor) and connection routing.
| Method | Description |
|---|---|
client.getMessageNode(peerUid) |
Gets or creates the bidirectional channel manager for the peer. |
node.send(data) |
Queues a raw binary message. The returned AFuture completes when the message is accepted for sending. |
client.onMessage.add(handler) |
Global event consumer for all incoming P2P messages (senderUid, payload). |
node.toConsumer(consumer) |
Binds a consumer to the node's incoming binary stream (bufferIn). |
The most advanced feature of MessageNode is its ability to convert a raw binary stream into a type-safe Remote Procedure Call (RPC) layer. This is essential for structured communication between clients.
The node.toApi(meta, localApi) method binds your local implementation of an API interface (localApi) to the incoming stream. When the peer sends a message (an RPC request), the binary data is automatically deserialized and invokes the corresponding method on your localApi object. Return values are then serialized and sent back.
This method is specifically designed to manage a complete, dedicated RPC context for the peer.
- Creates a FastMeta Context (
FastApiContextLocal). - Overrides the context's internal
flush()method to usenode.send(data)for all outgoing RPC requests. - Binds the incoming stream (
bufferIn) to the local API methods.
// Assume the interface and its FastMeta definition are ready:
import { MyApi, MyApiMeta } from './api_defs';
// 1. Create a MessageNode for the peer
const node = client.getMessageNode(peerUid);
// 2. Define the factory for the local API implementation
const localApiFactory = (remoteApi: MyApi) => {
return {
// Implement method: called when peer requests it
getPeerName: () => ARFuture.of("Peer's Name"),
// Implement method: called when peer requests it
logEvent: (level, msg) => { console.log(`[${level}] Peer: ${msg}`); return AFuture.of(); }
} as MyApi; // Cast to ensure type compatibility
};
// 3. Create the RPC Context using toApiR
// The result is the FastApiContext that handles both I/O
const rpcContext = node.toApiR(MyApiMeta, localApiFactory);
// If MyApi includes remote calls, they are now routed through rpcContext.flush() -> node.send()
// Example: Execute a remote call on the peer:
const remoteApi = rpcContext.remoteApi; // Access the peer's remote API interface
remoteApi.logEvent("INFO", "I am sending a remote event now");
rpcContext.flush(); // Triggers node.send() with the serialized callThe BMap<K, V> (Batching Map) is the client's reactive and network-aware cache layer. It is used for all core data lookups to ensure network efficiency.
| Component (BMap Instance) | Purpose |
|---|---|
client.servers |
Caches ServerDescriptors by ID. |
client.clouds |
Caches a peer's Cloud (list of server IDs) by UUID. |
client.accessGroups |
Caches AccessGroup definitions. |
client.accessCheckCache |
Caches the result of access permissions checks. |
- Requesting: Calling
bmap.getFuture(key)places thekeyinto the batch queue (allRequests). - Batching: The client's background scheduler (or an explicit
client.flush()) checks the queues of allBMapinstances. - Flushing: It extracts all pending keys from the queues via
bmap.getRequestsFor(sender)and sends a single RPC request (e.g.,resolveClouds(keys)orresolverServers(keys)) to the work server.
Access control mutations (adding/removing group members) are also batched using internal queues (client.accessOperationsAdd/Remove).
client.createAccessGroup(...uids): Creates a new group, returning a mutable interface (ARFuture<AccessGroupI>).accessGroupI.add(uuid)/accessGroupI.remove(uuid): Queues the operation into the appropriate map. The network operation is deferred until the nextclient.flush().client.checkAccess(uid1, uid2): Performs a batched lookup in theaccessCheckCache.
The client uses a unified contextual logger (Log) for structured and high-performance output.
Use the using syntax (with appropriate TypeScript configuration) or try/finally with Log.context() to attach structured data to all logs within a block.
import { Log, LogFilter, LogLevel } from '@aethernet/client-ts';
// Set up the printer (e.g., enable DEBUG and above)
Log.printConsoleColored(new LogFilter().notLevel(LogLevel.TRACE));
// Use context() to capture component details for all nested logs
using _l = Log.context({
component: "WorkConnection",
threadId: 5
});
{
Log.info("Starting login sequence");
try {
// ... network call ...
Log.debug("Serialization complete", { bytes: 1024 });
} catch (e) {
Log.error("Login failed unexpectedly", e as Error);
}
}
// Context is automatically popped.