@plexspaces/sdk 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -220,6 +220,121 @@ host.registry.heartbeat(actorId, RegistryObjectType.ACTOR);
220
220
 
221
221
  WIT TypeScript types are generated automatically as part of the SDK build process (`npm run build`). These types are optional and only used internally by the SDK for type checking. Client code doesn't need to generate or import these types - the SDK abstracts all WIT details away.
222
222
 
223
+ ## WsThinClient — Browser & Node.js Thin-Node SDK
224
+
225
+ `WsThinClient` connects a browser tab (or Node.js process) to a PlexSpaces node
226
+ as a **thin node** over a binary protobuf WebSocket connection. Zero runtime
227
+ dependencies — all wire encoding is hand-rolled in `src/wire/ws-frame-wire.ts`.
228
+
229
+ ### Thin Node vs Full Node
230
+
231
+ | | Full Node | Thin Node |
232
+ |---|---|---|
233
+ | Connection | gRPC or WebSocket | WebSocket only |
234
+ | Hosts WASM actors | ✓ | ✗ |
235
+ | Receives actor messages | ✓ | ✓ (via `onMessage`) |
236
+ | Sends tell/ask | ✓ | ✓ |
237
+ | Participates in SWIM | ✓ | ✗ (filtered out of indirect ping targets) |
238
+ | Node role | `NODE_ROLE_FULL` | `NODE_ROLE_THIN` |
239
+
240
+ ### Quick Start
241
+
242
+ ```typescript
243
+ import { WsThinClient, ActorID } from "@plexspaces/sdk";
244
+
245
+ const client = new WsThinClient({
246
+ wsUrl: "ws://localhost:8091/ws",
247
+ jwtToken: optionalJwt, // appended as ?token= query param
248
+ nodeId: WsThinClient.newUlid(), // ULID preferred; server assigns one if omitted
249
+ namespace: "my-app",
250
+ });
251
+
252
+ // Connect — sends NodeRegistration handshake, receives assigned node_id
253
+ const nodeId = await client.connect();
254
+
255
+ // Build a canonical actor ID on this thin node
256
+ const myActorId = client.localActorId("alice", "ChatClient", "my-app");
257
+ // → "alice//ChatClient::my-app@<assignedNodeId>"
258
+
259
+ // Build an ID for a server-side actor
260
+ const roomId = new ActorID("lobby", "ChatRoomActor", "my-app", leaderNodeId).toString();
261
+
262
+ // Fire-and-forget tell
263
+ await client.tell(roomId, "send", { sender_actor_id: myActorId, text: "hello" });
264
+
265
+ // Request-reply ask (5s timeout)
266
+ const reply = await client.ask(roomId, "status", {});
267
+
268
+ // Receive incoming tells addressed to this thin node
269
+ client.onMessage((actorId, msgType, payload) => {
270
+ if (msgType === "chat_message") console.log(payload);
271
+ });
272
+
273
+ // Ping another node for resource hints
274
+ const ping = await client.pingNode("other-node-id");
275
+ // → { success, nodeId, cpuPercent, memoryAvailableMb, availableCores }
276
+
277
+ await client.disconnect();
278
+ ```
279
+
280
+ ### Interface
281
+
282
+ ```typescript
283
+ export interface ThinClientOptions {
284
+ wsUrl: string; // "ws://host:port/ws"
285
+ jwtToken?: string; // optional Bearer token
286
+ nodeId?: string; // ULID preferred; auto-generated if omitted
287
+ tenant?: string; // placed in NodeRegistration.capabilities["tenant"]
288
+ namespace?: string; // placed in NodeRegistration.capabilities["namespace"]
289
+ }
290
+
291
+ // Typed view of PingResponse resource fields (proto fields 9–11).
292
+ // Not a proto message — TypeScript interface over decoded response.
293
+ export interface ThinNodePingResult {
294
+ success: boolean;
295
+ nodeId: string;
296
+ cpuPercent: number; // 0–100, from sysinfo on the target node
297
+ memoryAvailableMb: number; // available RAM in MiB
298
+ availableCores: number; // logical CPU count
299
+ }
300
+
301
+ export class WsThinClient {
302
+ constructor(opts: ThinClientOptions)
303
+
304
+ connect(): Promise<string> // returns assigned node_id; starts 25s heartbeat
305
+ tell(actorId, msgType, payload): Promise<void>
306
+ ask(actorId, msgType, payload, timeoutMs?): Promise<unknown>
307
+ onMessage(handler: (actorId, msgType, payload) => void): void
308
+ pingNode(targetNodeId, timeoutMs?): Promise<ThinNodePingResult>
309
+ heartbeat(): Promise<void>
310
+ localActorId(name, type, namespace?): string // "{name}//{type}::{ns}@{assignedNodeId}"
311
+ get nodeId(): string
312
+ static newUlid(): string // 26-char ULID, Crockford base32, crypto random
313
+ disconnect(): Promise<void>
314
+ }
315
+ ```
316
+
317
+ ### Wire Protocol
318
+
319
+ Binary protobuf `WsFrame` over WebSocket (`binaryType = 'arraybuffer'`).
320
+ Field numbers are defined in `proto/plexspaces/v1/transport/websocket.proto`
321
+ and documented in `src/wire/ws-frame-wire.ts`.
322
+
323
+ | Operation | WsFrame field | Direction |
324
+ |---|---|---|
325
+ | NodeRegistration handshake | field 20 → ACK field 21 | client → server → client |
326
+ | tell (fire-and-forget) | field 10 | client → server |
327
+ | tell_response | field 11 | server → client |
328
+ | ask (request-reply) | field 12 → response field 13 | client → server → client |
329
+ | heartbeat | field 24 → ACK field 25 | client → server → client |
330
+ | node_ping | field 22 → response field 23 | client → server → client |
331
+ | incoming_tell (server push) | field 10 | server → client |
332
+
333
+ ### Examples
334
+
335
+ - [`ws_chat_room`](../../examples/typescript/apps/ws_chat_room/README.md) — real-time chat via PG broadcast
336
+ - [`mersenne_prime`](../../examples/typescript/apps/mersenne_prime/README.md) — distributed computation with Web Workers
337
+
223
338
  ## License
224
339
 
225
340
  AGPL-3.0-or-later
package/dist/actor.js CHANGED
@@ -18,10 +18,7 @@
18
18
  * Type definitions are generated in src/generated/ but are not imported here
19
19
  * to keep the SDK simple. The virtual import works at runtime without types.
20
20
  */
21
- // @ts-ignore - Virtual import provided by jco componentize at runtime
22
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
23
- // @ts-expect-error - Virtual import - types are optional (generated in src/generated/)
24
- import { log as hostLog } from 'plexspaces:actor/host@0.1.0';
21
+ import { log as hostLog } from 'plexspaces:actor/host-logging@0.1.0';
25
22
  import { getActorDefinition } from './decorators.js';
26
23
  import { decodeWitPayloadUtf8, encodeWitPayloadUtf8 } from './wit-payload.js';
27
24
  /**
package/dist/host.d.ts CHANGED
@@ -119,12 +119,110 @@ export declare class Registry {
119
119
  heartbeat(objectId: string, objectType?: number, tenantId?: string, namespace?: string): void;
120
120
  }
121
121
  export { RegistryObjectType };
122
+ /**
123
+ * KVStore provides namespaced KV operations.
124
+ * Equivalent to Cloudflare DO's this.state.storage API.
125
+ * Access via host.kv.get(), host.kv.put() etc.
126
+ */
127
+ export declare class KVStore {
128
+ get(key: string): string | null;
129
+ put(key: string, value: string): void;
130
+ delete(key: string): void;
131
+ list(prefix: string): string[];
132
+ putWithTtl(key: string, value: string, ttlSeconds: number): void;
133
+ getTtl(key: string): number;
134
+ cas(key: string, expected: string | null, newValue: string): boolean;
135
+ increment(key: string, delta: number): number;
136
+ multiGet(keys: string[]): (string | null)[];
137
+ multiPut(entries: Record<string, string>): void;
138
+ getJson<T = unknown>(key: string): T | null;
139
+ putJson(key: string, value: unknown): void;
140
+ }
141
+ /**
142
+ * AlarmClient provides durable alarm scheduling.
143
+ * Equivalent to Cloudflare DO's setAlarm/getAlarm/deleteAlarm.
144
+ * Access via host.alarm.set(), host.alarm.get(), host.alarm.delete() etc.
145
+ */
146
+ export declare class AlarmClient {
147
+ set(timestampMs: number): void;
148
+ setIn(delayMs: number): void;
149
+ get(): number;
150
+ delete(): void;
151
+ }
152
+ /**
153
+ * LockClient provides distributed lock operations.
154
+ * Access via host.locks.acquire(), host.locks.release() etc.
155
+ */
156
+ export declare class LockClient {
157
+ acquire(holderId: string, lockName: string, leaseDurationSecs: number, timeoutMs: bigint): Uint8Array;
158
+ release(lockId: string, holderId: string, lockVersion: string): void;
159
+ renew(lockId: string, holderId: string, lockVersion: string, leaseDurationSecs: number): Uint8Array;
160
+ }
161
+ /**
162
+ * BlobClient provides blob storage operations.
163
+ * Access via host.blob.upload(), host.blob.download() etc.
164
+ */
165
+ export declare class BlobClient {
166
+ upload(name: string, data: Uint8Array, contentType: string): string;
167
+ download(blobId: string): Uint8Array;
168
+ delete(blobId: string): void;
169
+ list(prefix: string): string[];
170
+ }
171
+ /**
172
+ * ActorRef is a lightweight handle to a named virtual actor.
173
+ * Equivalent to Cloudflare DO's env.BINDING.get(id).
174
+ * Use getActorRef() to obtain one.
175
+ *
176
+ * @example
177
+ * ```typescript
178
+ * const room = getActorRef("ChatRoomActor", roomId, "default");
179
+ * const result = room.ask("send_message", payload, BigInt(5000));
180
+ * ```
181
+ */
182
+ export declare class ActorRef {
183
+ readonly actorId: string;
184
+ constructor(actorType: string, name: string, namespace: string);
185
+ id(): string;
186
+ tell(msgType: string, payload: Uint8Array): void;
187
+ ask(msgType: string, payload: Uint8Array, timeoutMs: bigint): Uint8Array;
188
+ }
189
+ /**
190
+ * getActorRef constructs an ActorRef for a named virtual actor.
191
+ * The actor is created automatically on first contact (virtual actor pattern).
192
+ * Equivalent to Cloudflare: `const room = env.CHAT_ROOM.get(roomId)`
193
+ */
194
+ export declare function getActorRef(actorType: string, name: string, namespace: string): ActorRef;
122
195
  export declare class Host {
123
196
  readonly processGroups: ProcessGroups;
124
197
  /** Tuple space: list-in, list-out. Use null in patterns for wildcards. */
125
198
  readonly ts: TupleSpace;
126
199
  /** Object registry: register, discover, and look up actors and services. */
127
200
  readonly registry: Registry;
201
+ /** Namespaced KV store: host.kv.get(), host.kv.put() etc. */
202
+ readonly kv: KVStore;
203
+ /** Durable alarm client: host.alarm.set(), host.alarm.get() etc. */
204
+ readonly alarm: AlarmClient;
205
+ /** Distributed lock client: host.locks.acquire(), host.locks.release() etc. */
206
+ readonly locks: LockClient;
207
+ /** Blob storage client: host.blob.upload(), host.blob.download() etc. */
208
+ readonly blob: BlobClient;
209
+ /**
210
+ * Create an ergonomic HTTP client for a named service link.
211
+ *
212
+ * The link must be pre-configured in RuntimeConfig.service_links.
213
+ * The host handles retries, circuit breaking, and auth injection.
214
+ *
215
+ * @param linkName - Service link name (e.g. "payments-api")
216
+ * @returns A {@link ServiceHttpClient} bound to that link
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * const http = host.httpClient("payments-api");
221
+ * const balance = http.get("/v1/balance?account=123");
222
+ * const result = http.post("/v1/transfer", { amount: 100 });
223
+ * ```
224
+ */
225
+ httpClient(linkName: string): ServiceHttpClient;
128
226
  /** Send message to another actor (fire-and-forget) */
129
227
  send(to: string, msgType: string, payload?: unknown): string;
130
228
  /** Send request and wait for response (request-reply) */
@@ -172,14 +270,6 @@ export declare class Host {
172
270
  error(message: string): void;
173
271
  /** Get current timestamp in milliseconds */
174
272
  nowMs(): number;
175
- kvGet(key: string): string;
176
- kvPut(key: string, value: string): string;
177
- kvDelete(key: string): string;
178
- kvList(prefix: string): string;
179
- /** Retrieve a JSON value by key. Returns parsed object or null if not found. */
180
- kvGetJson<T = unknown>(key: string): T | null;
181
- /** Serialize value to JSON and store under key. Throws on write failure. */
182
- kvPutJson(key: string, value: unknown): void;
183
273
  /** Increment a single named application metric counter by 1. Errors are swallowed. */
184
274
  incrCounter(applicationId: string, name: string): void;
185
275
  /** Increment one or more named application metric counters. Errors are swallowed. */
@@ -192,13 +282,6 @@ export declare class Host {
192
282
  tsTakePayload(data: Uint8Array): Uint8Array;
193
283
  /** @internal */
194
284
  tsReadAllPayload(data: Uint8Array): Uint8Array;
195
- lockAcquire(tenantId: string, namespace: string, holderId: string, lockName: string, leaseDurationSecs?: number, timeoutMs?: number): string;
196
- lockRelease(lockId: string, tenantId: string, namespace: string, holderId: string, lockVersion: string): string;
197
- lockRenew(lockId: string, tenantId: string, namespace: string, holderId: string, lockVersion: string, leaseDurationSecs?: number): string;
198
- blobUpload(blobId: string, data: string, contentType?: string): string;
199
- blobDownload(blobId: string): string;
200
- blobDelete(blobId: string): string;
201
- blobList(prefix: string): string;
202
285
  /**
203
286
  * Checkout an actor from a named pool. Returns handle { actor_id, pool_name, checkout_id } or null on failure.
204
287
  */
@@ -276,9 +359,13 @@ export declare class EventLog {
276
359
  * The link must be pre-configured in RuntimeConfig.service_links.
277
360
  * The host handles retries, circuit breaking, and auth injection.
278
361
  *
362
+ * Prefer creating instances via {@link Host.httpClient} rather than calling
363
+ * the constructor directly:
364
+ *
279
365
  * @example
280
366
  * ```typescript
281
- * const http = new ServiceHttpClient("payments-api");
367
+ * // Preferred: use the factory method
368
+ * const http = host.httpClient("payments-api");
282
369
  * const balance = http.get("/v1/balance?account=123");
283
370
  * const result = http.post("/v1/transfer", { amount: 100 });
284
371
  * ```