@plexspaces/sdk 0.1.1
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 +225 -0
- package/dist/actor.d.ts +81 -0
- package/dist/actor.js +484 -0
- package/dist/actor.test.d.ts +1 -0
- package/dist/actor.test.js +171 -0
- package/dist/actor_id.d.ts +46 -0
- package/dist/actor_id.js +85 -0
- package/dist/decorators.d.ts +40 -0
- package/dist/decorators.js +97 -0
- package/dist/host.d.ts +312 -0
- package/dist/host.js +828 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +14 -0
- package/dist/leader_worker.d.ts +36 -0
- package/dist/leader_worker.js +119 -0
- package/dist/process_groups.d.ts +4 -0
- package/dist/process_groups.js +14 -0
- package/dist/proto.d.ts +33 -0
- package/dist/proto.js +10 -0
- package/dist/router.d.ts +37 -0
- package/dist/router.js +78 -0
- package/dist/wire/http-fetch-proto-wire.d.ts +7 -0
- package/dist/wire/http-fetch-proto-wire.js +101 -0
- package/dist/wire/proto-wire-common.d.ts +18 -0
- package/dist/wire/proto-wire-common.js +91 -0
- package/dist/wire/registry-proto-wire.d.ts +41 -0
- package/dist/wire/registry-proto-wire.js +280 -0
- package/dist/wire/shard-group-proto-wire.d.ts +7 -0
- package/dist/wire/shard-group-proto-wire.js +723 -0
- package/dist/wire/tuplespace-proto-wire.d.ts +8 -0
- package/dist/wire/tuplespace-proto-wire.js +183 -0
- package/dist/wit-payload.d.ts +23 -0
- package/dist/wit-payload.js +42 -0
- package/dist/workflow.d.ts +14 -0
- package/dist/workflow.js +47 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# PlexSpaces TypeScript SDK
|
|
2
|
+
|
|
3
|
+
Build PlexSpaces actors with minimal boilerplate using **inheritance** (TypeScript analogue to the Python SDK’s decorators).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
From the repo (development):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd sdks/typescript
|
|
11
|
+
npm install
|
|
12
|
+
npm run build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Proto-shaped SDK types live in **`src/proto.ts`** and keep canonical proto field names even when generated files are not present yet. When you run **`make proto`** or **`make proto-typescript`**, richer generated models can be added on top, but application code can already stay proto-first at the SDK boundary.
|
|
16
|
+
|
|
17
|
+
The SDK now supports decorator-based authoring metadata in addition to the existing base classes:
|
|
18
|
+
|
|
19
|
+
- `@actor`
|
|
20
|
+
- `@gen_server_actor`
|
|
21
|
+
- `@event_actor`
|
|
22
|
+
- `@fsm_actor` / `@fsm_actor({ states: [...], initial: "..." })` — FSM actor with optional state list and initial state
|
|
23
|
+
- `@workflow_actor`
|
|
24
|
+
- `@run_handler`
|
|
25
|
+
- `@signal_handler("name")`
|
|
26
|
+
- `@query_handler("name")`
|
|
27
|
+
- `@handler`
|
|
28
|
+
|
|
29
|
+
These decorators align the TypeScript surface with the Rust and Python SDKs while keeping `PlexSpacesActor` and `WorkflowActor` as the runtime substrate.
|
|
30
|
+
|
|
31
|
+
In an example or app, add a dependency:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@plexspaces/sdk": "file:../../../../sdks/typescript"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tests
|
|
40
|
+
|
|
41
|
+
From the repo root, **`make test`** runs **`npm test`** here (`node --test`). Locally: `cd sdks/typescript && npm test`.
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
### 1. Extend `PlexSpacesActor`
|
|
46
|
+
|
|
47
|
+
Override `getDefaultState()` and add handlers as `on<Op>(payload)` methods. The base class wires WIT `init`, `handle`, `getState`, `setState` and dispatches by `payload.op`.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { PlexSpacesActor } from "@plexspaces/sdk";
|
|
51
|
+
|
|
52
|
+
interface MyState {
|
|
53
|
+
count: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class CounterActor extends PlexSpacesActor<MyState> {
|
|
57
|
+
getDefaultState(): MyState {
|
|
58
|
+
return { count: 0 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onIncrement(payload: Record<string, unknown>) {
|
|
62
|
+
const amount = Number(payload.amount ?? 1);
|
|
63
|
+
this.state.count += amount;
|
|
64
|
+
return { count: this.state.count };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onGet() {
|
|
68
|
+
return { count: this.state.count };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const instance = new CounterActor();
|
|
73
|
+
export const actor = {
|
|
74
|
+
init: (c: string) => instance.init(c),
|
|
75
|
+
handle: (from: string, msg: string, payload: string) => instance.handle(from, msg, payload),
|
|
76
|
+
getState: () => instance.getState(),
|
|
77
|
+
setState: (s: string) => instance.setState(s),
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Build to WASM
|
|
82
|
+
|
|
83
|
+
- Compile: `tsc`
|
|
84
|
+
- Bundle (e.g. esbuild) your entry into a single ESM file that exports `actor`.
|
|
85
|
+
- Build WASM: `jco componentize your-bundle.mjs --wit wit/plexspaces-actor -o actor.wasm --disable all`
|
|
86
|
+
|
|
87
|
+
See [examples/typescript/apps/bank_account](../../examples/typescript/apps/bank_account) for a full example and E2E test.
|
|
88
|
+
|
|
89
|
+
## Virtual Actors (Orleans-Style)
|
|
90
|
+
|
|
91
|
+
Virtual actors are automatically activated on first message and deactivated after idle timeout. Configure via `app-config.toml`:
|
|
92
|
+
|
|
93
|
+
```toml
|
|
94
|
+
[[supervisor.children]]
|
|
95
|
+
id = "my-actor"
|
|
96
|
+
type = "worker"
|
|
97
|
+
facets = [
|
|
98
|
+
{ type = "virtual_actor", priority = 100, config = { idle_timeout = "5m", activation_strategy = "lazy" } }
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The TypeScript SDK works seamlessly with virtual actors - no code changes needed. The framework handles activation/deactivation automatically.
|
|
103
|
+
|
|
104
|
+
## API
|
|
105
|
+
|
|
106
|
+
- **`PlexSpacesActor<TState>`**
|
|
107
|
+
Base class. `TState` is your state shape (plain object).
|
|
108
|
+
|
|
109
|
+
- **`getDefaultState(): TState`**
|
|
110
|
+
Override to return initial state.
|
|
111
|
+
|
|
112
|
+
- **`onInit(config: Record<string, unknown>): void`**
|
|
113
|
+
Optional. Called from `init()` with parsed config JSON.
|
|
114
|
+
|
|
115
|
+
- **Handlers**
|
|
116
|
+
Implement `on<Op>(payload)` for each message op (e.g. `onDeposit`, `onBalance`). The runtime calls `handle(from, msgType, payloadJson)` and the base class dispatches by `payload.op` (or `payload`) to `on` + capitalized op name. Return a JSON-serializable value; the base class serializes it (or `"ERROR:..."` on throw).
|
|
117
|
+
|
|
118
|
+
- **`protected state: TState`**
|
|
119
|
+
Current state; read/write in handlers.
|
|
120
|
+
|
|
121
|
+
- **`protected json(obj), error(msg)`**
|
|
122
|
+
Helpers for returning JSON or error strings.
|
|
123
|
+
|
|
124
|
+
## WIT Interface
|
|
125
|
+
|
|
126
|
+
The SDK targets the **plexspaces-actor** WIT world: `init`, `handle`, `get-state`, `set-state`. All data crosses the boundary as strings (JSON). See `wit/plexspaces-actor/world.wit`.
|
|
127
|
+
|
|
128
|
+
### Host Functions
|
|
129
|
+
|
|
130
|
+
The SDK uses WIT host functions for runtime capabilities. Host functions are provided by the PlexSpaces runtime when the WASM component is instantiated.
|
|
131
|
+
|
|
132
|
+
**Virtual Import Pattern**: jco componentize uses virtual imports for WIT host interfaces. The SDK handles these internally - you don't need to import them directly.
|
|
133
|
+
|
|
134
|
+
The SDK automatically handles logging via `host.log()` for error reporting and observability. In non-WASM environments (e.g., Node.js verification), host functions are undefined and logging gracefully degrades to no-op.
|
|
135
|
+
|
|
136
|
+
**Available Host Functions** (from `plexspaces:actor/host@0.1.0`):
|
|
137
|
+
- `log(level: string, message: string)` - Log a message (used internally by SDK)
|
|
138
|
+
- `send(to: string, msgType: string, payloadJson: string)` - Send message to another actor
|
|
139
|
+
- `now_ms()` - Get current timestamp in milliseconds
|
|
140
|
+
- `kv_get(key: string)`, `kv_put(key: string, value: string)`, etc. - Key-value storage
|
|
141
|
+
- **TupleSpace**: Prefer `host.ts` (list-in, list-out): `host.ts.write(tuple)`, `host.ts.take(pattern)` → tuple or null, `host.ts.readAll(pattern)` → tuple[]. Use `null` in patterns for wildcards. Low-level: `ts_write(tupleJson)`, `ts_take(patternJson)`, etc.
|
|
142
|
+
- **Process groups**: `host.processGroups.broadcast(group, msgType, payload)` — `msgType` is used by the host for routing; payload can be data-only.
|
|
143
|
+
- **Elastic pool**: `host.poolCheckout(poolName, timeoutMs)` → `{ actor_id, pool_name, checkout_id } | null`; `host.poolCheckin(poolName, actorId, checkoutId, healthy)`; `host.poolGetMetrics(poolName)` → metrics object or null. When the pool is not configured, checkout returns null; use process group broadcast as fallback. See [Parameter sweep (migrating_merlin)](../../examples/typescript/apps/migrating_merlin/README.md).
|
|
144
|
+
- **Object Registry**: `host.registry.register(reg)`, `host.registry.lookup(id, type)`, `host.registry.lookupByAlias(alias)`, `host.registry.discover(opts)`, `host.registry.heartbeat(id, type)`. All wire encoding is proto-first (see below).
|
|
145
|
+
- See `wit/plexspaces-actor/world.wit` for complete interface
|
|
146
|
+
|
|
147
|
+
**Note**: The SDK uses `host.log()` internally for error logging. For custom logging in your actors, you can import and use host functions directly using the virtual import pattern (future enhancement: SDK may provide helper methods).
|
|
148
|
+
|
|
149
|
+
### Tier 1 Ergonomics Helpers
|
|
150
|
+
|
|
151
|
+
Convenience wrappers on the `host` singleton and `EventLog` class.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { host, EventLog } from "@plexspaces/sdk";
|
|
155
|
+
|
|
156
|
+
// Process Groups — first member
|
|
157
|
+
const routerId: string | null = host.processGroups.first("svc:llm_router");
|
|
158
|
+
const routerIdStrict: string = host.processGroups.firstOrThrow("svc:llm_router");
|
|
159
|
+
|
|
160
|
+
// KV JSON helpers
|
|
161
|
+
interface Task { seq: number; kind: string; }
|
|
162
|
+
host.kvPutJson("task:1", { seq: 1, kind: "summarize" });
|
|
163
|
+
const task: Task | null = host.kvGetJson<Task>("task:1");
|
|
164
|
+
|
|
165
|
+
// Metrics (errors swallowed)
|
|
166
|
+
host.incrCounter("my-app", "requests_processed");
|
|
167
|
+
host.incrCounters("my-app", { cacheHits: 5, cacheMisses: 2 });
|
|
168
|
+
|
|
169
|
+
// EventLog — embed in actor state
|
|
170
|
+
class MyActor extends PlexSpacesActor<{ auditLog: EventLog }> {
|
|
171
|
+
getDefaultState() { return { auditLog: new EventLog() }; }
|
|
172
|
+
|
|
173
|
+
onRecord(payload: unknown) {
|
|
174
|
+
const seq = this.state.auditLog.append(host, "audit:", payload);
|
|
175
|
+
return { seq };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
onPoll(payload: { consumerId: string; limit?: number }) {
|
|
179
|
+
const [events, cursor] = this.state.auditLog.poll(
|
|
180
|
+
host, "audit:", payload.consumerId, payload.limit ?? 50
|
|
181
|
+
);
|
|
182
|
+
return { events, cursor };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Object Registry
|
|
188
|
+
|
|
189
|
+
Service registration and discovery via the host registry surface. The SDK handles protobuf wire encoding automatically; the proto schema is `proto/plexspaces/v1/registry/object_registry.proto`.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { host, RegistryObjectType } from "@plexspaces/sdk";
|
|
193
|
+
|
|
194
|
+
// Register this actor as a service
|
|
195
|
+
host.registry.register({
|
|
196
|
+
objectId: actorId,
|
|
197
|
+
objectType: "actor",
|
|
198
|
+
objectCategory: "worker",
|
|
199
|
+
grpcAddress: "http://node:8080",
|
|
200
|
+
capabilities: ["persistent"],
|
|
201
|
+
labels: ["env=prod"],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Lookup by object ID
|
|
205
|
+
const reg = host.registry.lookup(actorId, RegistryObjectType.ACTOR);
|
|
206
|
+
|
|
207
|
+
// Lookup by alias
|
|
208
|
+
const reg2 = host.registry.lookupByAlias("my-service-alias");
|
|
209
|
+
|
|
210
|
+
// Discover all actors of a type
|
|
211
|
+
const regs = host.registry.discover({ objectType: RegistryObjectType.ACTOR });
|
|
212
|
+
|
|
213
|
+
// Send heartbeat to refresh liveness
|
|
214
|
+
host.registry.heartbeat(actorId, RegistryObjectType.ACTOR);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Tenant isolation**: `tenant_id` is injected by the host from the deployment context (JWT for API deploys, or `tenant_id` in `app-config.toml` for file-copy deploys). Any `tenant_id` in the SDK call is silently overridden. `namespace` may be supplied; empty string falls back to the application's default namespace.
|
|
218
|
+
|
|
219
|
+
### Type Generation
|
|
220
|
+
|
|
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
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
AGPL-3.0-or-later
|
package/dist/actor.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for PlexSpaces actors (actor-world WIT: init, handle, get-state, set-state).
|
|
3
|
+
*
|
|
4
|
+
* Subclass and override:
|
|
5
|
+
* - getDefaultState(): initial state
|
|
6
|
+
* - onInit(config): optional, called from init() with parsed config
|
|
7
|
+
* - on<Op>(payload): handler for message op (e.g. onDeposit, onBalance).
|
|
8
|
+
* Op is derived from payload.op or payload; method name is "on" + capitalized op.
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* class BankAccountActor extends PlexSpacesActor<BankAccountState> {
|
|
12
|
+
* getDefaultState() { return { account_id: '', balance: 0, transactions: [] }; }
|
|
13
|
+
* onDeposit(p: { amount: number }) { ...; return { status: 'ok', balance: this.state.balance }; }
|
|
14
|
+
* onBalance() { return { account: this.state.account_id, balance: this.state.balance }; }
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export declare abstract class PlexSpacesActor<TState extends object = Record<string, unknown>> {
|
|
18
|
+
protected state: TState;
|
|
19
|
+
private cachedStateJson;
|
|
20
|
+
constructor();
|
|
21
|
+
/** Return initial state. Override in subclass. */
|
|
22
|
+
abstract getDefaultState(): TState;
|
|
23
|
+
/** Optional: called from init() with parsed config. Override to apply config to state. */
|
|
24
|
+
protected onInit(_config: Record<string, unknown>): void;
|
|
25
|
+
/**
|
|
26
|
+
* WIT `init(config: payload) -> result<_, actor-error>`.
|
|
27
|
+
* Success: return (unit). Failure: throw (jco maps throws to `err` for function-return `result`).
|
|
28
|
+
*/
|
|
29
|
+
init(configJson: string | Uint8Array | ArrayBuffer | ArrayBufferView): void;
|
|
30
|
+
/**
|
|
31
|
+
* WIT `handle(...) -> result<payload, actor-error>` (`payload` is `list<u8>` → `Uint8Array` in jco).
|
|
32
|
+
* Dispatches by msgType for Workflow behavior (workflow_run, workflow_signal:name, workflow_query:name),
|
|
33
|
+
* then by payload.op (or payload) to on<Op>(payload). Returns UTF-8 JSON bytes.
|
|
34
|
+
* Uses iterative serializer to avoid WASM recursion.
|
|
35
|
+
*
|
|
36
|
+
* Workflow behavior (aligned with Rust Workflow trait and Python @workflow_actor):
|
|
37
|
+
* - msgType "workflow_run" -> run(payload)
|
|
38
|
+
* - msgType "workflow_signal:name" -> signal(name, payload)
|
|
39
|
+
* - msgType "workflow_query:name" -> query(name, payload)
|
|
40
|
+
*/
|
|
41
|
+
handle(_fromActor: string, msgType: string, payloadJson: string | Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array;
|
|
42
|
+
/** WIT `get-state() -> result<payload, actor-error>`. Returns JSON state as UTF-8 bytes. */
|
|
43
|
+
getState(): Uint8Array;
|
|
44
|
+
/** WIT `set-state(state: payload) -> result<_, actor-error>`. */
|
|
45
|
+
setState(stateJson: string | Uint8Array | ArrayBuffer | ArrayBufferView): void;
|
|
46
|
+
protected capitalize(s: string): string;
|
|
47
|
+
protected resolveDecoratedHandler(op: string, definition?: import("./decorators.js").ActorDefinitionMetadata | undefined): string | null;
|
|
48
|
+
/**
|
|
49
|
+
* Serialize object to JSON string using fully iterative approach (zero recursion).
|
|
50
|
+
*
|
|
51
|
+
* jco componentize compiles JS to WASM (StarlingMonkey) with a tiny call stack.
|
|
52
|
+
* Native JSON.stringify recurses per-element and per-nesting-level, hitting
|
|
53
|
+
* stack limits with arrays of 2+ items. This iterative serializer uses a work
|
|
54
|
+
* stack instead of recursive function calls.
|
|
55
|
+
*/
|
|
56
|
+
protected json(obj: unknown): string;
|
|
57
|
+
protected error(message: string): string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Base class for Workflow behavior actors (aligned with Rust Workflow trait and Python @workflow_actor).
|
|
61
|
+
*
|
|
62
|
+
* Implement run(), signal(), and query() for durable workflows (Temporal/Restate-style).
|
|
63
|
+
* Message routing: framework sends msgType "workflow_run" | "workflow_signal:name" | "workflow_query:name";
|
|
64
|
+
* PlexSpacesActor.handle() dispatches to these methods when present.
|
|
65
|
+
*
|
|
66
|
+
* Example:
|
|
67
|
+
* class OrderFulfillmentActor extends WorkflowActor<OrderState> {
|
|
68
|
+
* getDefaultState() { return { orderId: '', status: 'pending', steps: [] }; }
|
|
69
|
+
* run(payload: Record<string, unknown>) { ...; return { status: 'completed' }; }
|
|
70
|
+
* signal(name: string, data: Record<string, unknown>) { if (name === 'cancel') this.state.status = 'cancelled'; }
|
|
71
|
+
* query(name: string, params: Record<string, unknown>) { if (name === 'status') return this.state; return {}; }
|
|
72
|
+
* }
|
|
73
|
+
*/
|
|
74
|
+
export declare abstract class WorkflowActor<TState extends object = Record<string, unknown>> extends PlexSpacesActor<TState> {
|
|
75
|
+
/** Main workflow execution (exclusive). Called when msgType is "workflow_run". Return result for ask/call. */
|
|
76
|
+
abstract run(payload: Record<string, unknown>): Record<string, unknown> | unknown;
|
|
77
|
+
/** Handle external signal (e.g. cancel). Called when msgType is "workflow_signal:name". */
|
|
78
|
+
abstract signal(name: string, data: Record<string, unknown>): void;
|
|
79
|
+
/** Read-only query. Called when msgType is "workflow_query:name". Return result for ask/call. */
|
|
80
|
+
abstract query(name: string, params: Record<string, unknown>): Record<string, unknown> | unknown;
|
|
81
|
+
}
|