@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 +115 -0
- package/dist/actor.js +1 -4
- package/dist/host.d.ts +103 -16
- package/dist/host.js +316 -101
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/wire/ws-frame-wire.d.ts +75 -0
- package/dist/wire/ws-frame-wire.js +412 -0
- package/dist/ws_thin_client.d.ts +79 -0
- package/dist/ws_thin_client.js +302 -0
- package/package.json +1 -1
- package/dist/actor.test.d.ts +0 -1
- package/dist/actor.test.js +0 -171
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
|
-
|
|
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
|
-
*
|
|
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
|
* ```
|