@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/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { PlexSpacesActor, WorkflowActor } from "./actor.js";
|
|
2
|
+
export { decodeWitPayloadUtf8, encodeWitPayloadUtf8 } from "./wit-payload.js";
|
|
3
|
+
export { actor, gen_server_actor, event_actor, fsm_actor, workflow_actor, handler, init_handler, run_handler, signal_handler, query_handler, getActorDefinition, } from "./decorators.js";
|
|
4
|
+
export { Host, ProcessGroups, TupleSpace, host, pgFirst, pgFirstOrThrow, ServiceHttpClient } from "./host.js";
|
|
5
|
+
export { ActorRouter } from "./router.js";
|
|
6
|
+
export { defaultRetryConfig, withRetry, type RetryConfig, } from "./workflow.js";
|
|
7
|
+
export { LeaderWorkerClient, listWorkerNodeIds, } from "./leader_worker.js";
|
|
8
|
+
export { ActorID } from "./actor_id.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
//
|
|
4
|
+
// PlexSpaces TypeScript SDK
|
|
5
|
+
//
|
|
6
|
+
// Build actors with minimal boilerplate via inheritance (mirrors Python SDK's decorators).
|
|
7
|
+
export { PlexSpacesActor, WorkflowActor } from "./actor.js";
|
|
8
|
+
export { decodeWitPayloadUtf8, encodeWitPayloadUtf8 } from "./wit-payload.js";
|
|
9
|
+
export { actor, gen_server_actor, event_actor, fsm_actor, workflow_actor, handler, init_handler, run_handler, signal_handler, query_handler, getActorDefinition, } from "./decorators.js";
|
|
10
|
+
export { Host, ProcessGroups, TupleSpace, host, pgFirst, pgFirstOrThrow, ServiceHttpClient } from "./host.js";
|
|
11
|
+
export { ActorRouter } from "./router.js";
|
|
12
|
+
export { defaultRetryConfig, withRetry, } from "./workflow.js";
|
|
13
|
+
export { LeaderWorkerClient, listWorkerNodeIds, } from "./leader_worker.js";
|
|
14
|
+
export { ActorID } from "./actor_id.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client for leader-worker multi-node patterns. Connect to the entry (leader) node
|
|
3
|
+
* via HTTP; list worker node IDs and spawn non-virtual actors on specific nodes.
|
|
4
|
+
* Virtual actors are created lazily on first message—no explicit spawn or ensure.
|
|
5
|
+
*/
|
|
6
|
+
export declare class LeaderWorkerClient {
|
|
7
|
+
private readonly entryUrl;
|
|
8
|
+
private readonly nodeIdToHttp;
|
|
9
|
+
/**
|
|
10
|
+
* @param entryHttpUrl - Base URL of the entry/leader node (e.g. "http://localhost:8092").
|
|
11
|
+
*/
|
|
12
|
+
constructor(entryHttpUrl: string);
|
|
13
|
+
/**
|
|
14
|
+
* Derive HTTP URL from gRPC node_address (convention: HTTP = gRPC port + 1).
|
|
15
|
+
*/
|
|
16
|
+
private static grpcPortToHttp;
|
|
17
|
+
/**
|
|
18
|
+
* List node IDs that can run workers (peers + self). Populates internal
|
|
19
|
+
* cache so spawnActorOnNode can resolve nodeId to node HTTP URL.
|
|
20
|
+
*
|
|
21
|
+
* @returns List of node_id strings.
|
|
22
|
+
*/
|
|
23
|
+
listWorkerNodeIds(cluster?: string | null, pageSize?: number, pageToken?: string): Promise<string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Spawn a non-virtual actor on a specific node. The node must be reachable;
|
|
26
|
+
* call listWorkerNodeIds first so the client can resolve nodeId to URL.
|
|
27
|
+
*
|
|
28
|
+
* @returns Actor ref string (e.g. "worker-ulid@node_id").
|
|
29
|
+
*/
|
|
30
|
+
spawnActorOnNode(nodeId: string, actorType: string, actorId?: string, initialState?: Uint8Array, config?: Record<string, unknown> | null, labels?: Record<string, string> | null): Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convenience: list worker node IDs using a one-off client.
|
|
34
|
+
* For multiple calls or spawnActorOnNode, use LeaderWorkerClient.
|
|
35
|
+
*/
|
|
36
|
+
export declare function listWorkerNodeIds(entryHttpUrl: string, cluster?: string | null, pageSize?: number): Promise<string[]>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
//
|
|
4
|
+
// Leader-worker client for multi-node: same API surface as Rust/Python/Go SDKs.
|
|
5
|
+
// Virtual actors are created lazily on first message; use spawnActorOnNode
|
|
6
|
+
// only for non-virtual workers.
|
|
7
|
+
/**
|
|
8
|
+
* Client for leader-worker multi-node patterns. Connect to the entry (leader) node
|
|
9
|
+
* via HTTP; list worker node IDs and spawn non-virtual actors on specific nodes.
|
|
10
|
+
* Virtual actors are created lazily on first message—no explicit spawn or ensure.
|
|
11
|
+
*/
|
|
12
|
+
export class LeaderWorkerClient {
|
|
13
|
+
/**
|
|
14
|
+
* @param entryHttpUrl - Base URL of the entry/leader node (e.g. "http://localhost:8092").
|
|
15
|
+
*/
|
|
16
|
+
constructor(entryHttpUrl) {
|
|
17
|
+
this.nodeIdToHttp = new Map();
|
|
18
|
+
this.entryUrl = entryHttpUrl.replace(/\/$/, "");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Derive HTTP URL from gRPC node_address (convention: HTTP = gRPC port + 1).
|
|
22
|
+
*/
|
|
23
|
+
static grpcPortToHttp(url) {
|
|
24
|
+
if (!url)
|
|
25
|
+
return url;
|
|
26
|
+
const match = url.trim().match(/:(\d+)\s*$/);
|
|
27
|
+
if (match) {
|
|
28
|
+
const port = parseInt(match[1], 10) + 1;
|
|
29
|
+
return url.trim().replace(/:(\d+)\s*$/, `:${port}`);
|
|
30
|
+
}
|
|
31
|
+
return url;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* List node IDs that can run workers (peers + self). Populates internal
|
|
35
|
+
* cache so spawnActorOnNode can resolve nodeId to node HTTP URL.
|
|
36
|
+
*
|
|
37
|
+
* @returns List of node_id strings.
|
|
38
|
+
*/
|
|
39
|
+
async listWorkerNodeIds(cluster, pageSize = 100, pageToken = "") {
|
|
40
|
+
const params = new URLSearchParams({ pageSize: String(pageSize) });
|
|
41
|
+
if (cluster)
|
|
42
|
+
params.set("cluster", cluster);
|
|
43
|
+
if (pageToken)
|
|
44
|
+
params.set("pageToken", pageToken);
|
|
45
|
+
const url = `${this.entryUrl}/api/v1/nodes?${params.toString()}`;
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method: "GET",
|
|
48
|
+
headers: { Accept: "application/json" },
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const body = await res.text();
|
|
52
|
+
throw new Error(`listWorkerNodeIds failed: ${res.status} ${body}`);
|
|
53
|
+
}
|
|
54
|
+
const data = (await res.json());
|
|
55
|
+
const nodes = data.nodes ?? data.nodeRegistrations ?? [];
|
|
56
|
+
const ids = [];
|
|
57
|
+
for (const n of nodes) {
|
|
58
|
+
const nodeId = n.nodeId ?? n.node_id ?? "";
|
|
59
|
+
const nodeAddress = n.nodeAddress ?? n.node_address ?? "";
|
|
60
|
+
if (nodeId) {
|
|
61
|
+
ids.push(nodeId);
|
|
62
|
+
if (nodeAddress) {
|
|
63
|
+
this.nodeIdToHttp.set(nodeId, LeaderWorkerClient.grpcPortToHttp(nodeAddress));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return ids;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Spawn a non-virtual actor on a specific node. The node must be reachable;
|
|
71
|
+
* call listWorkerNodeIds first so the client can resolve nodeId to URL.
|
|
72
|
+
*
|
|
73
|
+
* @returns Actor ref string (e.g. "worker-ulid@node_id").
|
|
74
|
+
*/
|
|
75
|
+
async spawnActorOnNode(nodeId, actorType, actorId = "", initialState = new Uint8Array(0), config, labels) {
|
|
76
|
+
const nodeHttp = this.nodeIdToHttp.get(nodeId);
|
|
77
|
+
if (!nodeHttp) {
|
|
78
|
+
throw new Error(`unknown nodeId "${nodeId}"; call listWorkerNodeIds first`);
|
|
79
|
+
}
|
|
80
|
+
const payload = { actorType };
|
|
81
|
+
if (actorId)
|
|
82
|
+
payload.actorId = actorId;
|
|
83
|
+
if (initialState.byteLength > 0) {
|
|
84
|
+
let binary = "";
|
|
85
|
+
for (let i = 0; i < initialState.length; i++) {
|
|
86
|
+
binary += String.fromCharCode(initialState[i]);
|
|
87
|
+
}
|
|
88
|
+
payload.initialState = btoa(binary);
|
|
89
|
+
}
|
|
90
|
+
if (config != null)
|
|
91
|
+
payload.config = config;
|
|
92
|
+
if (labels != null)
|
|
93
|
+
payload.labels = labels;
|
|
94
|
+
const body = JSON.stringify(payload);
|
|
95
|
+
const url = `${nodeHttp.replace(/\/$/, "")}/api/v1/actors/spawn`;
|
|
96
|
+
const res = await fetch(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
99
|
+
body,
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
throw new Error(`spawnActorOnNode failed: ${res.status} ${text}`);
|
|
104
|
+
}
|
|
105
|
+
const out = (await res.json());
|
|
106
|
+
const actorRef = out.actorRef ?? out.actor_ref ?? "";
|
|
107
|
+
if (!actorRef)
|
|
108
|
+
throw new Error("spawnActorOnNode returned empty actorRef");
|
|
109
|
+
return actorRef;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Convenience: list worker node IDs using a one-off client.
|
|
114
|
+
* For multiple calls or spawnActorOnNode, use LeaderWorkerClient.
|
|
115
|
+
*/
|
|
116
|
+
export async function listWorkerNodeIds(entryHttpUrl, cluster, pageSize = 100) {
|
|
117
|
+
const client = new LeaderWorkerClient(entryHttpUrl);
|
|
118
|
+
return client.listWorkerNodeIds(cluster, pageSize);
|
|
119
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Return the first process-group member, or null if the group is empty. */
|
|
2
|
+
export declare function firstGroupMember(members: string[]): string | null;
|
|
3
|
+
/** Return the first process-group member, throwing if the group is empty. */
|
|
4
|
+
export declare function firstGroupMemberOrThrow(group: string, members: string[]): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
/** Return the first process-group member, or null if the group is empty. */
|
|
4
|
+
export function firstGroupMember(members) {
|
|
5
|
+
return members.length > 0 ? members[0] : null;
|
|
6
|
+
}
|
|
7
|
+
/** Return the first process-group member, throwing if the group is empty. */
|
|
8
|
+
export function firstGroupMemberOrThrow(group, members) {
|
|
9
|
+
const first = firstGroupMember(members);
|
|
10
|
+
if (first === null) {
|
|
11
|
+
throw new Error(`no members in process group '${group}'`);
|
|
12
|
+
}
|
|
13
|
+
return first;
|
|
14
|
+
}
|
package/dist/proto.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface RetryConfig {
|
|
2
|
+
max_attempts?: number;
|
|
3
|
+
initial_interval_ms?: number;
|
|
4
|
+
backoff_rate?: number;
|
|
5
|
+
max_interval_ms?: number;
|
|
6
|
+
retryable_errors?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface Facet {
|
|
9
|
+
type: string;
|
|
10
|
+
config?: Record<string, string>;
|
|
11
|
+
priority?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Message {
|
|
14
|
+
id?: string;
|
|
15
|
+
message_type?: string;
|
|
16
|
+
payload?: Uint8Array | string;
|
|
17
|
+
}
|
|
18
|
+
export interface RequestContext {
|
|
19
|
+
tenant_id?: string;
|
|
20
|
+
namespace?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ErrorDetail {
|
|
23
|
+
code?: string;
|
|
24
|
+
message?: string;
|
|
25
|
+
details?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface ActorConfig {
|
|
28
|
+
max_mailbox_size?: number;
|
|
29
|
+
enable_persistence?: boolean;
|
|
30
|
+
actor_groups?: string[];
|
|
31
|
+
properties?: Record<string, unknown>;
|
|
32
|
+
config_schema_version?: number;
|
|
33
|
+
}
|
package/dist/proto.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
//
|
|
4
|
+
// Proto-shaped SDK types.
|
|
5
|
+
//
|
|
6
|
+
// The repository can generate richer TypeScript models via `make proto-typescript`,
|
|
7
|
+
// but the SDK itself must still build in environments where those generated files
|
|
8
|
+
// are absent. These interfaces preserve the canonical proto field names so
|
|
9
|
+
// application code stays proto-first at the SDK boundary.
|
|
10
|
+
export {};
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PlexSpacesActor } from "./actor.js";
|
|
2
|
+
/** Factory function that creates an actor instance. */
|
|
3
|
+
type ActorFactory = () => PlexSpacesActor;
|
|
4
|
+
/**
|
|
5
|
+
* Routes messages to multiple actor types within a single WASM module.
|
|
6
|
+
*
|
|
7
|
+
* Register each actor type under its exact class name, which matches what
|
|
8
|
+
* the framework places in ``config.actor_type``:
|
|
9
|
+
*
|
|
10
|
+
* const router = new ActorRouter({
|
|
11
|
+
* "ChatRoomActor": () => new ChatRoomActor(),
|
|
12
|
+
* "RateLimiterActor": () => new RateLimiterActor(),
|
|
13
|
+
* });
|
|
14
|
+
* export const actor = router;
|
|
15
|
+
*
|
|
16
|
+
* For same-class multi-instance dispatch, register under the role name:
|
|
17
|
+
*
|
|
18
|
+
* const router = new ActorRouter({
|
|
19
|
+
* "AbstractionsActor": () => new AbstractionsActor(),
|
|
20
|
+
* "ephemeral": () => new AbstractionsActor(),
|
|
21
|
+
* "channel": () => new AbstractionsActor(),
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
24
|
+
export declare class ActorRouter {
|
|
25
|
+
private readonly factories;
|
|
26
|
+
private active;
|
|
27
|
+
constructor(routes: Record<string, ActorFactory>);
|
|
28
|
+
/** WIT `init(config: payload) -> result<_, actor-error>` */
|
|
29
|
+
init(configJson: string | Uint8Array | ArrayBuffer | ArrayBufferView): void;
|
|
30
|
+
/** WIT `handle(...) -> result<payload, actor-error>` */
|
|
31
|
+
handle(fromActor: string, msgType: string, payloadJson: string | Uint8Array | ArrayBuffer | ArrayBufferView): Uint8Array;
|
|
32
|
+
/** WIT `get-state() -> result<payload, actor-error>` */
|
|
33
|
+
getState(): Uint8Array;
|
|
34
|
+
/** WIT `set-state(state: payload) -> result<_, actor-error>` */
|
|
35
|
+
setState(stateJson: string | Uint8Array | ArrayBuffer | ArrayBufferView): void;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
//
|
|
4
|
+
// PlexSpaces TypeScript SDK - Multi-Actor Router
|
|
5
|
+
//
|
|
6
|
+
// Routes messages to multiple actor types within a single WASM module.
|
|
7
|
+
//
|
|
8
|
+
// Dispatch rule (matches Python and Go SDKs):
|
|
9
|
+
// 1. config.actor_type — exact lookup (the normal case; always set by the framework).
|
|
10
|
+
// 2. config.role — exact lookup (same-actor_type multi-instance only,
|
|
11
|
+
// e.g. "ephemeral"/"channel" both map to AbstractionsActor).
|
|
12
|
+
// 3. Error — no silent fallback; a missing registration is always a bug.
|
|
13
|
+
import { decodeWitPayloadUtf8, encodeWitPayloadUtf8 } from "./wit-payload.js";
|
|
14
|
+
/**
|
|
15
|
+
* Routes messages to multiple actor types within a single WASM module.
|
|
16
|
+
*
|
|
17
|
+
* Register each actor type under its exact class name, which matches what
|
|
18
|
+
* the framework places in ``config.actor_type``:
|
|
19
|
+
*
|
|
20
|
+
* const router = new ActorRouter({
|
|
21
|
+
* "ChatRoomActor": () => new ChatRoomActor(),
|
|
22
|
+
* "RateLimiterActor": () => new RateLimiterActor(),
|
|
23
|
+
* });
|
|
24
|
+
* export const actor = router;
|
|
25
|
+
*
|
|
26
|
+
* For same-class multi-instance dispatch, register under the role name:
|
|
27
|
+
*
|
|
28
|
+
* const router = new ActorRouter({
|
|
29
|
+
* "AbstractionsActor": () => new AbstractionsActor(),
|
|
30
|
+
* "ephemeral": () => new AbstractionsActor(),
|
|
31
|
+
* "channel": () => new AbstractionsActor(),
|
|
32
|
+
* });
|
|
33
|
+
*/
|
|
34
|
+
export class ActorRouter {
|
|
35
|
+
constructor(routes) {
|
|
36
|
+
this.active = null;
|
|
37
|
+
this.factories = routes;
|
|
38
|
+
}
|
|
39
|
+
/** WIT `init(config: payload) -> result<_, actor-error>` */
|
|
40
|
+
init(configJson) {
|
|
41
|
+
const text = decodeWitPayloadUtf8(configJson);
|
|
42
|
+
const config = text.trim() ? JSON.parse(text) : {};
|
|
43
|
+
const actorType = config.actor_type || "";
|
|
44
|
+
const role = config.role || "";
|
|
45
|
+
// 1. actor_type — exact match (primary dispatch key set by framework)
|
|
46
|
+
let factory = actorType ? this.factories[actorType] : undefined;
|
|
47
|
+
// 2. role — exact match (same-actor_type multi-instance only)
|
|
48
|
+
if (!factory && role) {
|
|
49
|
+
factory = this.factories[role];
|
|
50
|
+
}
|
|
51
|
+
if (!factory) {
|
|
52
|
+
throw new Error(`ERROR: no actor registered for actor_type='${actorType}' role='${role}'`);
|
|
53
|
+
}
|
|
54
|
+
this.active = factory();
|
|
55
|
+
this.active.init(text);
|
|
56
|
+
}
|
|
57
|
+
/** WIT `handle(...) -> result<payload, actor-error>` */
|
|
58
|
+
handle(fromActor, msgType, payloadJson) {
|
|
59
|
+
if (!this.active) {
|
|
60
|
+
return encodeWitPayloadUtf8('{"error":"no active actor (init not called)"}');
|
|
61
|
+
}
|
|
62
|
+
return this.active.handle(fromActor, msgType, payloadJson);
|
|
63
|
+
}
|
|
64
|
+
/** WIT `get-state() -> result<payload, actor-error>` */
|
|
65
|
+
getState() {
|
|
66
|
+
if (!this.active) {
|
|
67
|
+
return encodeWitPayloadUtf8("{}");
|
|
68
|
+
}
|
|
69
|
+
return this.active.getState();
|
|
70
|
+
}
|
|
71
|
+
/** WIT `set-state(state: payload) -> result<_, actor-error>` */
|
|
72
|
+
setState(stateJson) {
|
|
73
|
+
if (!this.active) {
|
|
74
|
+
throw new Error("ERROR: no active actor");
|
|
75
|
+
}
|
|
76
|
+
this.active.setState(stateJson);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Build HttpFetchRequest: repeated map entry (field 1), body (field 2). */
|
|
2
|
+
export declare function encodeHttpFetchRequestWire(headers: Record<string, string>, body: Uint8Array | null): Uint8Array;
|
|
3
|
+
export declare function decodeHttpFetchResponseWire(data: Uint8Array): {
|
|
4
|
+
status: number;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
body: string;
|
|
7
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
//
|
|
4
|
+
// Manual protobuf wire for plexspaces.wasm.v1 HttpFetchRequest / HttpFetchResponse.
|
|
5
|
+
// Matches sdks/go/plexspaces/http_fetch_proto_wire.go.
|
|
6
|
+
import { appendLengthDelimited, readLengthDelimited, readVarint, skipField } from './proto-wire-common.js';
|
|
7
|
+
function utf8Valid(bytes) {
|
|
8
|
+
try {
|
|
9
|
+
new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function bytesToBase64Sync(bytes) {
|
|
17
|
+
let bin = '';
|
|
18
|
+
for (let i = 0; i < bytes.length; i++)
|
|
19
|
+
bin += String.fromCharCode(bytes[i]);
|
|
20
|
+
if (typeof btoa !== 'undefined')
|
|
21
|
+
return btoa(bin);
|
|
22
|
+
const Buf = globalThis.Buffer;
|
|
23
|
+
if (Buf)
|
|
24
|
+
return Buf.from(bytes).toString('base64');
|
|
25
|
+
throw new Error('base64 encode unavailable');
|
|
26
|
+
}
|
|
27
|
+
/** Build HttpFetchRequest: repeated map entry (field 1), body (field 2). */
|
|
28
|
+
export function encodeHttpFetchRequestWire(headers, body) {
|
|
29
|
+
let buf = new Uint8Array(0);
|
|
30
|
+
const enc = new TextEncoder();
|
|
31
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
32
|
+
const kb = new Uint8Array(enc.encode(k));
|
|
33
|
+
const vb = new Uint8Array(enc.encode(v));
|
|
34
|
+
let entry = appendLengthDelimited(new Uint8Array(0), 1, kb);
|
|
35
|
+
entry = appendLengthDelimited(entry, 2, vb);
|
|
36
|
+
buf = appendLengthDelimited(buf, 1, entry);
|
|
37
|
+
}
|
|
38
|
+
const bodyUse = body && body.length > 0 ? new Uint8Array(body) : new Uint8Array(0);
|
|
39
|
+
buf = appendLengthDelimited(buf, 2, bodyUse);
|
|
40
|
+
return buf;
|
|
41
|
+
}
|
|
42
|
+
function parseStringStringMapEntry(entry) {
|
|
43
|
+
let pos = 0;
|
|
44
|
+
let key = '';
|
|
45
|
+
let val = '';
|
|
46
|
+
while (pos < entry.length) {
|
|
47
|
+
const { value: tag, n: tn } = readVarint(entry, pos);
|
|
48
|
+
pos += tn;
|
|
49
|
+
const fn = Number(tag >> 3n);
|
|
50
|
+
const wt = Number(tag & 7n);
|
|
51
|
+
if (fn === 1 && wt === 2) {
|
|
52
|
+
const { slice, nextPos } = readLengthDelimited(entry, pos);
|
|
53
|
+
pos = nextPos;
|
|
54
|
+
key = new TextDecoder('utf-8', { fatal: false }).decode(slice);
|
|
55
|
+
}
|
|
56
|
+
else if (fn === 2 && wt === 2) {
|
|
57
|
+
const { slice, nextPos } = readLengthDelimited(entry, pos);
|
|
58
|
+
pos = nextPos;
|
|
59
|
+
val = new TextDecoder('utf-8', { fatal: false }).decode(slice);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
pos = skipField(entry, pos, wt);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { key, val };
|
|
66
|
+
}
|
|
67
|
+
export function decodeHttpFetchResponseWire(data) {
|
|
68
|
+
const out = {
|
|
69
|
+
status: 0,
|
|
70
|
+
headers: {},
|
|
71
|
+
body: '',
|
|
72
|
+
};
|
|
73
|
+
let pos = 0;
|
|
74
|
+
while (pos < data.length) {
|
|
75
|
+
const { value: tag, n: tn } = readVarint(data, pos);
|
|
76
|
+
pos += tn;
|
|
77
|
+
const fn = Number(tag >> 3n);
|
|
78
|
+
const wt = Number(tag & 7n);
|
|
79
|
+
if (fn === 1 && wt === 0) {
|
|
80
|
+
const { value: v, n: m } = readVarint(data, pos);
|
|
81
|
+
pos += m;
|
|
82
|
+
out.status = Number(v);
|
|
83
|
+
}
|
|
84
|
+
else if (fn === 2 && wt === 2) {
|
|
85
|
+
const { slice: sl, nextPos } = readLengthDelimited(data, pos);
|
|
86
|
+
pos = nextPos;
|
|
87
|
+
const { key, val } = parseStringStringMapEntry(sl);
|
|
88
|
+
if (key)
|
|
89
|
+
out.headers[key] = val;
|
|
90
|
+
}
|
|
91
|
+
else if (fn === 3 && wt === 2) {
|
|
92
|
+
const { slice: sl, nextPos } = readLengthDelimited(data, pos);
|
|
93
|
+
pos = nextPos;
|
|
94
|
+
out.body = utf8Valid(sl) ? new TextDecoder('utf-8', { fatal: false }).decode(sl) : bytesToBase64Sync(sl);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
pos = skipField(data, pos, wt);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Append non-negative protobuf varint (field tags, lengths, enums). */
|
|
2
|
+
export declare function appendVarint(buf: Uint8Array, xIn: number): Uint8Array<ArrayBuffer>;
|
|
3
|
+
export declare function appendFixed64LE(buf: Uint8Array, v: number): Uint8Array<ArrayBuffer>;
|
|
4
|
+
export declare function appendLengthDelimited(buf: Uint8Array, fieldNum: number, inner: Uint8Array): Uint8Array<ArrayBuffer>;
|
|
5
|
+
/**
|
|
6
|
+
* Concatenate byte buffers into a new ArrayBuffer-backed Uint8Array.
|
|
7
|
+
* Explicit return type avoids TS 5.9+ inferring ArrayBufferLike from subarrays / TextEncoder.
|
|
8
|
+
*/
|
|
9
|
+
export declare function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array<ArrayBuffer>;
|
|
10
|
+
export declare function readVarint(data: Uint8Array, pos: number): {
|
|
11
|
+
value: bigint;
|
|
12
|
+
n: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function skipField(data: Uint8Array, pos: number, wireType: number): number;
|
|
15
|
+
export declare function readLengthDelimited(data: Uint8Array, pos: number): {
|
|
16
|
+
slice: Uint8Array;
|
|
17
|
+
nextPos: number;
|
|
18
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
//
|
|
4
|
+
// Shared protobuf wire primitives for WASM host payloads.
|
|
5
|
+
// Matches sdks/go/plexspaces/tuplespace_proto_wire.go and http_fetch_proto_wire.go.
|
|
6
|
+
/** Append non-negative protobuf varint (field tags, lengths, enums). */
|
|
7
|
+
export function appendVarint(buf, xIn) {
|
|
8
|
+
if (!Number.isFinite(xIn) || xIn < 0 || xIn > Number.MAX_SAFE_INTEGER) {
|
|
9
|
+
throw new Error('appendVarint expects a non-negative safe integer');
|
|
10
|
+
}
|
|
11
|
+
let n = BigInt(Math.floor(xIn));
|
|
12
|
+
const parts = [];
|
|
13
|
+
while (n >= 0x80n) {
|
|
14
|
+
parts.push(Number(n & 0xffn) | 0x80);
|
|
15
|
+
n >>= 7n;
|
|
16
|
+
}
|
|
17
|
+
parts.push(Number(n));
|
|
18
|
+
return concatBytes(buf, new Uint8Array(parts));
|
|
19
|
+
}
|
|
20
|
+
export function appendFixed64LE(buf, v) {
|
|
21
|
+
const out = new Uint8Array(buf.length + 8);
|
|
22
|
+
out.set(buf, 0);
|
|
23
|
+
const view = new DataView(out.buffer, buf.length, 8);
|
|
24
|
+
view.setFloat64(0, v, true);
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export function appendLengthDelimited(buf, fieldNum, inner) {
|
|
28
|
+
const tag = BigInt(fieldNum << 3 | 2);
|
|
29
|
+
let b = appendVarint(buf, Number(tag));
|
|
30
|
+
b = appendVarint(b, inner.length);
|
|
31
|
+
return concatBytes(b, inner);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Concatenate byte buffers into a new ArrayBuffer-backed Uint8Array.
|
|
35
|
+
* Explicit return type avoids TS 5.9+ inferring ArrayBufferLike from subarrays / TextEncoder.
|
|
36
|
+
*/
|
|
37
|
+
export function concatBytes(a, b) {
|
|
38
|
+
const out = new Uint8Array(a.length + b.length);
|
|
39
|
+
out.set(a, 0);
|
|
40
|
+
out.set(b, a.length);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
export function readVarint(data, pos) {
|
|
44
|
+
let x = 0n;
|
|
45
|
+
let s = 0n;
|
|
46
|
+
const orig = pos;
|
|
47
|
+
for (let i = 0; i < 10; i++) {
|
|
48
|
+
if (pos >= data.length)
|
|
49
|
+
throw new Error('varint buffer underflow');
|
|
50
|
+
const b = data[pos];
|
|
51
|
+
pos++;
|
|
52
|
+
if (b < 0x80) {
|
|
53
|
+
return { value: x | (BigInt(b) << s), n: pos - orig };
|
|
54
|
+
}
|
|
55
|
+
x |= BigInt(b & 0x7f) << s;
|
|
56
|
+
s += 7n;
|
|
57
|
+
}
|
|
58
|
+
throw new Error('varint too long');
|
|
59
|
+
}
|
|
60
|
+
export function skipField(data, pos, wireType) {
|
|
61
|
+
switch (wireType) {
|
|
62
|
+
case 0: {
|
|
63
|
+
const { n } = readVarint(data, pos);
|
|
64
|
+
return pos + n;
|
|
65
|
+
}
|
|
66
|
+
case 1:
|
|
67
|
+
if (pos + 8 > data.length)
|
|
68
|
+
throw new Error('fixed64 underflow');
|
|
69
|
+
return pos + 8;
|
|
70
|
+
case 2: {
|
|
71
|
+
const { value: ln, n } = readVarint(data, pos);
|
|
72
|
+
return pos + n + Number(ln);
|
|
73
|
+
}
|
|
74
|
+
case 5:
|
|
75
|
+
if (pos + 4 > data.length)
|
|
76
|
+
throw new Error('fixed32 underflow');
|
|
77
|
+
return pos + 4;
|
|
78
|
+
default:
|
|
79
|
+
throw new Error(`unknown wire type ${wireType}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function readLengthDelimited(data, pos) {
|
|
83
|
+
const { value: ln, n } = readVarint(data, pos);
|
|
84
|
+
const start = pos + n;
|
|
85
|
+
const end = start + Number(ln);
|
|
86
|
+
if (end > data.length)
|
|
87
|
+
throw new Error('length-delimited field truncated');
|
|
88
|
+
const copy = new Uint8Array(end - start);
|
|
89
|
+
copy.set(data.subarray(start, end));
|
|
90
|
+
return { slice: copy, nextPos: end };
|
|
91
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type WireU8 = Uint8Array<ArrayBuffer>;
|
|
2
|
+
export declare const ObjectType: {
|
|
3
|
+
readonly UNSPECIFIED: 0;
|
|
4
|
+
readonly ACTOR: 1;
|
|
5
|
+
readonly TUPLESPACE: 2;
|
|
6
|
+
readonly SERVICE: 3;
|
|
7
|
+
readonly VM: 4;
|
|
8
|
+
readonly APPLICATION: 5;
|
|
9
|
+
readonly WORKFLOW: 6;
|
|
10
|
+
readonly NODE: 7;
|
|
11
|
+
readonly PROCESS_GROUP: 8;
|
|
12
|
+
};
|
|
13
|
+
export type ObjectTypeValue = typeof ObjectType[keyof typeof ObjectType];
|
|
14
|
+
export interface ObjectRegistration {
|
|
15
|
+
objectId: string;
|
|
16
|
+
objectType: string;
|
|
17
|
+
grpcAddress?: string;
|
|
18
|
+
objectCategory?: string;
|
|
19
|
+
tenantId?: string;
|
|
20
|
+
namespace?: string;
|
|
21
|
+
capabilities?: string[];
|
|
22
|
+
labels?: string[];
|
|
23
|
+
alias?: string;
|
|
24
|
+
healthStatus?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function encodeRegisterRequest(reg: ObjectRegistration): WireU8;
|
|
27
|
+
export declare function encodeUnregisterRequest(objectId: string, objectType: number, tenantId?: string, namespace?: string): WireU8;
|
|
28
|
+
export declare function encodeLookupRequest(objectId: string, objectType: number, tenantId?: string, namespace?: string, alias?: string): WireU8;
|
|
29
|
+
export declare function encodeDiscoverRequest(opts: {
|
|
30
|
+
objectType?: number;
|
|
31
|
+
objectCategory?: string;
|
|
32
|
+
tenantId?: string;
|
|
33
|
+
namespace?: string;
|
|
34
|
+
capabilities?: string[];
|
|
35
|
+
labels?: string[];
|
|
36
|
+
pageSize?: number;
|
|
37
|
+
}): WireU8;
|
|
38
|
+
export declare function encodeHeartbeatRequest(objectId: string, objectType: number, tenantId?: string, namespace?: string): WireU8;
|
|
39
|
+
export declare function decodeLookupResponse(data: Uint8Array): ObjectRegistration | null;
|
|
40
|
+
export declare function decodeDiscoverResponse(data: Uint8Array): ObjectRegistration[];
|
|
41
|
+
export {};
|