@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/host.js
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2025 PlexSpaces Contributors
|
|
3
|
+
//
|
|
4
|
+
// PlexSpaces Host Functions (TypeScript SDK)
|
|
5
|
+
//
|
|
6
|
+
// Provides TypeScript wrappers for WIT host imports.
|
|
7
|
+
// Uses virtual imports from 'plexspaces:actor/host@0.1.0'.
|
|
8
|
+
// Protobuf wire for WIT `payload` fields (matches Go WASM hostWire* / tuplespace_proto_wire).
|
|
9
|
+
import { decodeReadResponseAllTuples, decodeReadResponseFirstTuple, encodeReadRequest, encodeWriteRequest, } from './wire/tuplespace-proto-wire.js';
|
|
10
|
+
import { decodeHttpFetchResponseWire, encodeHttpFetchRequestWire } from './wire/http-fetch-proto-wire.js';
|
|
11
|
+
import { decodeApplicationMetrics, decodeCreateShardGroupResponse, decodeGetApplicationStatusResponse, decodeScatterGatherResponse, encodeApplicationMetrics, encodeCreateShardGroupRequest, encodeScatterGatherRequest, } from './wire/shard-group-proto-wire.js';
|
|
12
|
+
import { decodeWitPayloadUtf8, encodeWitPayloadUtf8 } from './wit-payload.js';
|
|
13
|
+
import { encodeRegisterRequest, encodeUnregisterRequest, encodeLookupRequest, encodeDiscoverRequest, encodeHeartbeatRequest, decodeLookupResponse, decodeDiscoverResponse, ObjectType as RegistryObjectType, } from './wire/registry-proto-wire.js';
|
|
14
|
+
import { firstGroupMember, firstGroupMemberOrThrow } from './process_groups.js';
|
|
15
|
+
// Virtual imports provided by jco componentize at runtime.
|
|
16
|
+
// These map 1:1 to the WIT host interface functions.
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
import { send as hostSend, ask as hostAsk, log as hostLog, nowMs as hostNowMs, selfId as hostSelfId, spawn as hostSpawn, stop as hostStop, link as hostLink, unlink as hostUnlink, monitor as hostMonitor, demonitor as hostDemonitor, sendAfter as hostSendAfter, kvGet as hostKvGet, kvPut as hostKvPut, kvDelete as hostKvDelete, kvList as hostKvList, tsWrite as hostTsWrite, tsRead as hostTsRead, tsTake as hostTsTake, tsReadAll as hostTsReadAll, lockAcquire as hostLockAcquire, lockRelease as hostLockRelease, lockRenew as hostLockRenew, blobUpload as hostBlobUpload, blobDownload as hostBlobDownload, blobDelete as hostBlobDelete, blobList as hostBlobList, pgJoin as hostPgJoin, pgLeave as hostPgLeave, pgMembers as hostPgMembers, pgBroadcast as hostPgBroadcast, poolCheckout as hostPoolCheckout, poolCheckin as hostPoolCheckin, poolGetMetrics as hostPoolGetMetrics, createShardGroup as hostCreateShardGroup, bulkUpdateShardGroup as hostBulkUpdateShardGroup, mapShardGroup as hostMapShardGroup, scatterGather as hostScatterGather, broadcastShardGroup as hostBroadcastShardGroup, reduceShardGroup as hostReduceShardGroup, allReduceShardGroup as hostAllReduceShardGroup, barrierShardGroup as hostBarrierShardGroup, spawnActors as hostSpawnActors, applicationMetricsAdd as hostApplicationMetricsAdd, applicationGetMetrics as hostApplicationGetMetrics, applicationGetStatus as hostApplicationGetStatus, httpFetch as hostHttpFetch,
|
|
19
|
+
// @ts-expect-error Virtual import
|
|
20
|
+
} from 'plexspaces:actor/host@0.1.0';
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
import { register as hostRegistryRegister, unregister as hostRegistryUnregister, lookup as hostRegistryLookup, lookupByAlias as hostRegistryLookupByAlias, discover as hostRegistryDiscover, heartbeat as hostRegistryHeartbeat,
|
|
23
|
+
// @ts-expect-error Virtual import
|
|
24
|
+
} from 'plexspaces:actor/registry@0.1.0';
|
|
25
|
+
/**
|
|
26
|
+
* Safe call helper — returns empty string if function is undefined.
|
|
27
|
+
*/
|
|
28
|
+
function safeCall(fn, ...args) {
|
|
29
|
+
if (typeof fn === 'function') {
|
|
30
|
+
return fn(...args);
|
|
31
|
+
}
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
/** Host list<u8> / payload: accept Uint8Array; some bindings surface Latin-1 string bytes. */
|
|
35
|
+
function hostPayloadToBytes(result) {
|
|
36
|
+
if (result instanceof Uint8Array)
|
|
37
|
+
return result;
|
|
38
|
+
// Cross-realm ArrayBufferView check: jco componentize-js returns list<u8> as a Uint8Array
|
|
39
|
+
// from a different JS realm inside the WASM component, so instanceof fails. Use isView() instead.
|
|
40
|
+
if (ArrayBuffer.isView(result)) {
|
|
41
|
+
const v = result;
|
|
42
|
+
return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
|
43
|
+
}
|
|
44
|
+
if (result instanceof ArrayBuffer) {
|
|
45
|
+
return new Uint8Array(result);
|
|
46
|
+
}
|
|
47
|
+
if (typeof result === 'string') {
|
|
48
|
+
const out = new Uint8Array(result.length);
|
|
49
|
+
for (let i = 0; i < result.length; i++)
|
|
50
|
+
out[i] = result.charCodeAt(i) & 0xff;
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
return new Uint8Array(0);
|
|
54
|
+
}
|
|
55
|
+
function hostErrorPrefixBytes(raw) {
|
|
56
|
+
const prefix = 'ERROR:';
|
|
57
|
+
if (raw.length < prefix.length)
|
|
58
|
+
return false;
|
|
59
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
60
|
+
if (raw[i] !== prefix.charCodeAt(i))
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Tuple space helper: list-in, list-out API.
|
|
67
|
+
* Use null in patterns for wildcards. Consistent with Python host.ts and Go host.TS().
|
|
68
|
+
*/
|
|
69
|
+
export class TupleSpace {
|
|
70
|
+
constructor(host) {
|
|
71
|
+
this.host = host;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Write a tuple. Values are encoded as plexspaces.tuplespace.v1 WriteRequest protobuf wire
|
|
75
|
+
* (same as Go `TupleSpace.Write` / Rust simple_component_host).
|
|
76
|
+
*/
|
|
77
|
+
write(tuple) {
|
|
78
|
+
try {
|
|
79
|
+
const wire = encodeWriteRequest(tuple);
|
|
80
|
+
return this.host.tsWritePayload(wire);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
return `ERROR: ${e instanceof Error ? e.message : String(e)}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Take one matching tuple (destructive). */
|
|
87
|
+
take(pattern) {
|
|
88
|
+
try {
|
|
89
|
+
const wire = encodeReadRequest(pattern, true, 1);
|
|
90
|
+
const raw = this.host.tsTakePayload(wire);
|
|
91
|
+
if (raw.length === 0 || hostErrorPrefixBytes(raw))
|
|
92
|
+
return null;
|
|
93
|
+
return decodeReadResponseFirstTuple(raw);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Read one matching tuple (non-destructive). */
|
|
100
|
+
read(pattern) {
|
|
101
|
+
try {
|
|
102
|
+
const wire = encodeReadRequest(pattern, false, 1);
|
|
103
|
+
const raw = this.host.tsReadPayload(wire);
|
|
104
|
+
if (raw.length === 0 || hostErrorPrefixBytes(raw))
|
|
105
|
+
return null;
|
|
106
|
+
return decodeReadResponseFirstTuple(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Read all matching tuples (non-destructive). */
|
|
113
|
+
readAll(pattern) {
|
|
114
|
+
try {
|
|
115
|
+
const wire = encodeReadRequest(pattern, false, 1024);
|
|
116
|
+
const raw = this.host.tsReadAllPayload(wire);
|
|
117
|
+
if (raw.length === 0 || hostErrorPrefixBytes(raw))
|
|
118
|
+
return [];
|
|
119
|
+
return decodeReadResponseAllTuples(raw);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Process groups sub-API.
|
|
128
|
+
* broadcast(group, msgType, payload): msgType is used by the host for routing; payload can be data-only.
|
|
129
|
+
*/
|
|
130
|
+
export class ProcessGroups {
|
|
131
|
+
/** Join a named process group */
|
|
132
|
+
join(group) {
|
|
133
|
+
const result = safeCall(hostPgJoin, group);
|
|
134
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
135
|
+
throw new Error(result);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Leave a named process group */
|
|
139
|
+
leave(group) {
|
|
140
|
+
const result = safeCall(hostPgLeave, group);
|
|
141
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
142
|
+
throw new Error(result);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Get members of a process group */
|
|
146
|
+
members(group) {
|
|
147
|
+
const raw = safeCall(hostPgMembers, group);
|
|
148
|
+
const result = decodeWitPayloadUtf8(raw);
|
|
149
|
+
if (result.startsWith('ERROR:')) {
|
|
150
|
+
throw new Error(result);
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(result);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Broadcast to all group members. msgType is used for routing so payload can be data-only. */
|
|
160
|
+
broadcast(group, msgType, payload) {
|
|
161
|
+
const payloadBytes = encodeWitPayloadUtf8(payload !== undefined ? JSON.stringify(payload) : '{}');
|
|
162
|
+
const result = safeCall(hostPgBroadcast, group, msgType, payloadBytes);
|
|
163
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
164
|
+
throw new Error(result);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Return the first member of a process group, or null if empty. */
|
|
168
|
+
first(group) {
|
|
169
|
+
return firstGroupMember(this.members(group));
|
|
170
|
+
}
|
|
171
|
+
/** Return the first member of a process group, throwing if empty. */
|
|
172
|
+
firstOrThrow(group) {
|
|
173
|
+
return firstGroupMemberOrThrow(group, this.members(group));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Object Registry host functions for registration and discovery.
|
|
178
|
+
*
|
|
179
|
+
* All calls encode request/response as proto wire bytes matching
|
|
180
|
+
* plexspaces.object_registry.v1 messages.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* // Register this actor
|
|
185
|
+
* host.registry.register({ objectId: myId, objectType: 'actor', objectCategory: 'GenServer' });
|
|
186
|
+
*
|
|
187
|
+
* // Look up by alias (Orleans grain directory pattern)
|
|
188
|
+
* const reg = host.registry.lookupByAlias('Counter:my-counter:default:tenant1');
|
|
189
|
+
*
|
|
190
|
+
* // Discover all actors of a given category
|
|
191
|
+
* const actors = host.registry.discover({ objectType: RegistryObjectType.ACTOR, objectCategory: 'GenServer' });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export class Registry {
|
|
195
|
+
/**
|
|
196
|
+
* Register an object in the registry.
|
|
197
|
+
*/
|
|
198
|
+
register(reg) {
|
|
199
|
+
const reqBytes = encodeRegisterRequest(reg);
|
|
200
|
+
const result = safeCall(hostRegistryRegister, reqBytes);
|
|
201
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
202
|
+
throw new Error(result);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Unregister an object from the registry.
|
|
207
|
+
*/
|
|
208
|
+
unregister(objectId, objectType, tenantId, namespace) {
|
|
209
|
+
const reqBytes = encodeUnregisterRequest(objectId, objectType, tenantId, namespace);
|
|
210
|
+
const result = safeCall(hostRegistryUnregister, reqBytes);
|
|
211
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
212
|
+
throw new Error(result);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Look up an object by ID. Returns null if not found, throws on storage errors.
|
|
217
|
+
*/
|
|
218
|
+
lookup(objectId, objectType = 0, tenantId, namespace) {
|
|
219
|
+
const reqBytes = encodeLookupRequest(objectId, objectType, tenantId, namespace);
|
|
220
|
+
const raw = safeCall(hostRegistryLookup, reqBytes);
|
|
221
|
+
if (typeof raw === 'string' && raw.startsWith('ERROR:')) {
|
|
222
|
+
throw new Error(raw);
|
|
223
|
+
}
|
|
224
|
+
if (!raw)
|
|
225
|
+
return null;
|
|
226
|
+
const bytes = raw instanceof Uint8Array ? raw : new Uint8Array(0);
|
|
227
|
+
if (bytes.length === 0)
|
|
228
|
+
return null;
|
|
229
|
+
return decodeLookupResponse(bytes);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Look up an object by alias (Orleans grain directory pattern).
|
|
233
|
+
* Alias format: "{actor_type}:{name}:{namespace}:{tenant_id}"
|
|
234
|
+
* Returns null if not found, throws on storage errors.
|
|
235
|
+
*/
|
|
236
|
+
lookupByAlias(alias) {
|
|
237
|
+
const raw = safeCall(hostRegistryLookupByAlias, alias);
|
|
238
|
+
if (typeof raw === 'string' && raw.startsWith('ERROR:')) {
|
|
239
|
+
throw new Error(raw);
|
|
240
|
+
}
|
|
241
|
+
if (!raw)
|
|
242
|
+
return null;
|
|
243
|
+
const bytes = raw instanceof Uint8Array ? raw : new Uint8Array(0);
|
|
244
|
+
if (bytes.length === 0)
|
|
245
|
+
return null;
|
|
246
|
+
return decodeLookupResponse(bytes);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Discover objects with optional filtering.
|
|
250
|
+
*/
|
|
251
|
+
discover(options = {}) {
|
|
252
|
+
const reqBytes = encodeDiscoverRequest(options);
|
|
253
|
+
const raw = safeCall(hostRegistryDiscover, reqBytes);
|
|
254
|
+
if (!raw)
|
|
255
|
+
return [];
|
|
256
|
+
if (typeof raw === 'string' && raw.startsWith('ERROR:')) {
|
|
257
|
+
throw new Error(raw);
|
|
258
|
+
}
|
|
259
|
+
const bytes = raw instanceof Uint8Array ? raw : new Uint8Array(0);
|
|
260
|
+
if (bytes.length === 0)
|
|
261
|
+
return [];
|
|
262
|
+
return decodeDiscoverResponse(bytes);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Update the heartbeat for a registered object.
|
|
266
|
+
*/
|
|
267
|
+
heartbeat(objectId, objectType = 0, tenantId, namespace) {
|
|
268
|
+
const reqBytes = encodeHeartbeatRequest(objectId, objectType, tenantId, namespace);
|
|
269
|
+
const result = safeCall(hostRegistryHeartbeat, reqBytes);
|
|
270
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
271
|
+
throw new Error(result);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export { RegistryObjectType };
|
|
276
|
+
export class Host {
|
|
277
|
+
constructor() {
|
|
278
|
+
this.processGroups = new ProcessGroups();
|
|
279
|
+
/** Tuple space: list-in, list-out. Use null in patterns for wildcards. */
|
|
280
|
+
this.ts = new TupleSpace(this);
|
|
281
|
+
/** Object registry: register, discover, and look up actors and services. */
|
|
282
|
+
this.registry = new Registry();
|
|
283
|
+
}
|
|
284
|
+
// ========================================================================
|
|
285
|
+
// Messaging
|
|
286
|
+
// ========================================================================
|
|
287
|
+
/** Send message to another actor (fire-and-forget) */
|
|
288
|
+
send(to, msgType, payload) {
|
|
289
|
+
const payloadBytes = encodeWitPayloadUtf8(payload !== undefined ? JSON.stringify(payload) : '');
|
|
290
|
+
const raw = safeCall(hostSend, to, msgType, payloadBytes);
|
|
291
|
+
// WIT `result<_, actor-error>` ok is unit; jco may yield `undefined` on success.
|
|
292
|
+
if (typeof raw !== 'string') {
|
|
293
|
+
return '';
|
|
294
|
+
}
|
|
295
|
+
return raw;
|
|
296
|
+
}
|
|
297
|
+
/** Send request and wait for response (request-reply) */
|
|
298
|
+
ask(to, msgType, payload, timeoutMs = 5000) {
|
|
299
|
+
const payloadBytes = encodeWitPayloadUtf8(payload !== undefined ? JSON.stringify(payload) : '');
|
|
300
|
+
const raw = safeCall(hostAsk, to, msgType, payloadBytes, BigInt(timeoutMs));
|
|
301
|
+
// jco returns result<list<u8>, actor-error>: OK value is Uint8Array, error is a string.
|
|
302
|
+
const result = decodeWitPayloadUtf8(raw);
|
|
303
|
+
if (result.startsWith('ERROR:')) {
|
|
304
|
+
throw new Error(result);
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(result);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// ========================================================================
|
|
314
|
+
// Actor Identity
|
|
315
|
+
// ========================================================================
|
|
316
|
+
/** Get own actor ID */
|
|
317
|
+
selfId() {
|
|
318
|
+
return safeCall(hostSelfId);
|
|
319
|
+
}
|
|
320
|
+
// ========================================================================
|
|
321
|
+
// Actor Lifecycle
|
|
322
|
+
// ========================================================================
|
|
323
|
+
/**
|
|
324
|
+
* Spawn a new actor through the framework-owned actor spawn path exposed by the host.
|
|
325
|
+
* @param moduleRef - Actor type/module reference (must be deployed)
|
|
326
|
+
* @param actorId - Unique ID for the new actor (empty = auto-generated ULID)
|
|
327
|
+
* @param initConfig - Optional config passed to the new actor's init()
|
|
328
|
+
* @returns Spawned actor ID string (may be auto-generated if actorId was empty)
|
|
329
|
+
*/
|
|
330
|
+
spawn(moduleRef, actorId = '', initConfig) {
|
|
331
|
+
const configBytes = encodeWitPayloadUtf8(initConfig !== undefined ? JSON.stringify(initConfig) : '{}');
|
|
332
|
+
const result = safeCall(hostSpawn, moduleRef, actorId, configBytes);
|
|
333
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
334
|
+
throw new Error(result);
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
/** Stop an actor gracefully */
|
|
339
|
+
stop(actorId) {
|
|
340
|
+
const result = safeCall(hostStop, actorId);
|
|
341
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
342
|
+
throw new Error(result);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// ========================================================================
|
|
346
|
+
// Actor Linking & Monitoring
|
|
347
|
+
// ========================================================================
|
|
348
|
+
/** Bidirectional link */
|
|
349
|
+
link(actorId) {
|
|
350
|
+
const result = safeCall(hostLink, actorId);
|
|
351
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
352
|
+
throw new Error(result);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/** Remove bidirectional link */
|
|
356
|
+
unlink(actorId) {
|
|
357
|
+
const result = safeCall(hostUnlink, actorId);
|
|
358
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
359
|
+
throw new Error(result);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** Monitor an actor (returns monitor reference) */
|
|
363
|
+
monitor(actorId) {
|
|
364
|
+
const result = safeCall(hostMonitor, actorId);
|
|
365
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
366
|
+
throw new Error(result);
|
|
367
|
+
}
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
/** Cancel a monitor */
|
|
371
|
+
demonitor(monitorRef) {
|
|
372
|
+
const result = safeCall(hostDemonitor, monitorRef);
|
|
373
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
374
|
+
throw new Error(result);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// ========================================================================
|
|
378
|
+
// Timers
|
|
379
|
+
// ========================================================================
|
|
380
|
+
/**
|
|
381
|
+
* Send message to self after delay (returns timer ID for tracking).
|
|
382
|
+
* Timer cancellation is managed by the framework's TimerFacet/ReminderFacet.
|
|
383
|
+
* Stop the actor to cancel pending timers.
|
|
384
|
+
*
|
|
385
|
+
* WIT `payload` is opaque bytes; pass UTF-8 JSON bytes so the host matches Go/Rust guest JSON.
|
|
386
|
+
*/
|
|
387
|
+
sendAfter(delayMs, msgType, payload) {
|
|
388
|
+
const text = payload !== undefined ? JSON.stringify(payload) : '{}';
|
|
389
|
+
const payloadBytes = new TextEncoder().encode(text);
|
|
390
|
+
const raw = safeCall(hostSendAfter, BigInt(delayMs), msgType, payloadBytes);
|
|
391
|
+
if (typeof raw === 'string') {
|
|
392
|
+
return raw;
|
|
393
|
+
}
|
|
394
|
+
if (raw && typeof raw === 'object') {
|
|
395
|
+
const o = raw;
|
|
396
|
+
if (o.tag === 'ok' || o.tag === 0) {
|
|
397
|
+
return typeof o.val === 'string' ? o.val : '';
|
|
398
|
+
}
|
|
399
|
+
if (o.tag === 'err' || o.tag === 1) {
|
|
400
|
+
return `ERROR:${String(o.val ?? 'send-after failed')}`;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return '';
|
|
404
|
+
}
|
|
405
|
+
// ========================================================================
|
|
406
|
+
// Logging & Time
|
|
407
|
+
// ========================================================================
|
|
408
|
+
/** Log a message */
|
|
409
|
+
log(level, message) {
|
|
410
|
+
safeCall(hostLog, level, message);
|
|
411
|
+
}
|
|
412
|
+
debug(message) { this.log('debug', message); }
|
|
413
|
+
info(message) { this.log('info', message); }
|
|
414
|
+
warn(message) { this.log('warn', message); }
|
|
415
|
+
error(message) { this.log('error', message); }
|
|
416
|
+
/** Get current timestamp in milliseconds */
|
|
417
|
+
nowMs() {
|
|
418
|
+
const result = safeCall(hostNowMs);
|
|
419
|
+
return typeof result === 'bigint' ? Number(result) : (typeof result === 'number' ? result : 0);
|
|
420
|
+
}
|
|
421
|
+
// ========================================================================
|
|
422
|
+
// Key-Value Store
|
|
423
|
+
// ========================================================================
|
|
424
|
+
kvGet(key) {
|
|
425
|
+
if (typeof hostKvGet !== 'function')
|
|
426
|
+
return '';
|
|
427
|
+
try {
|
|
428
|
+
return decodeWitPayloadUtf8(hostKvGet(key));
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
return `ERROR:${e}`;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
kvPut(key, value) {
|
|
435
|
+
if (typeof hostKvPut !== 'function')
|
|
436
|
+
return '';
|
|
437
|
+
try {
|
|
438
|
+
hostKvPut(key, encodeWitPayloadUtf8(value));
|
|
439
|
+
return '';
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
return `ERROR:${e}`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
kvDelete(key) {
|
|
446
|
+
if (typeof hostKvDelete !== 'function')
|
|
447
|
+
return '';
|
|
448
|
+
try {
|
|
449
|
+
hostKvDelete(key);
|
|
450
|
+
return '';
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
return `ERROR:${e}`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
kvList(prefix) {
|
|
457
|
+
if (typeof hostKvList !== 'function')
|
|
458
|
+
return '[]';
|
|
459
|
+
try {
|
|
460
|
+
return JSON.stringify(hostKvList(prefix));
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
return `ERROR:${e}`;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/** Retrieve a JSON value by key. Returns parsed object or null if not found. */
|
|
467
|
+
kvGetJson(key) {
|
|
468
|
+
const raw = this.kvGet(key);
|
|
469
|
+
if (!raw || raw.startsWith('ERROR:'))
|
|
470
|
+
return null;
|
|
471
|
+
try {
|
|
472
|
+
return JSON.parse(raw);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/** Serialize value to JSON and store under key. Throws on write failure. */
|
|
479
|
+
kvPutJson(key, value) {
|
|
480
|
+
const serialized = JSON.stringify(value);
|
|
481
|
+
const result = this.kvPut(key, serialized);
|
|
482
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
483
|
+
throw new Error(`kvPutJson(${key}): ${result}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/** Increment a single named application metric counter by 1. Errors are swallowed. */
|
|
487
|
+
incrCounter(applicationId, name) {
|
|
488
|
+
this.incrCounters(applicationId, { [name]: 1 });
|
|
489
|
+
}
|
|
490
|
+
/** Increment one or more named application metric counters. Errors are swallowed. */
|
|
491
|
+
incrCounters(applicationId, counters) {
|
|
492
|
+
try {
|
|
493
|
+
this.applicationMetricsAdd(applicationId, {
|
|
494
|
+
message_count: Object.keys(counters).length,
|
|
495
|
+
counter_metrics: counters,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch (e) {
|
|
499
|
+
this.warn(`incrCounters: metrics update failed: ${e}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// ========================================================================
|
|
503
|
+
// TupleSpace (protobuf WriteRequest / ReadRequest / ReadResponse wire bytes)
|
|
504
|
+
// ========================================================================
|
|
505
|
+
/** @internal TupleSpace — plexspaces.tuplespace.v1 wire bytes. */
|
|
506
|
+
tsWritePayload(data) {
|
|
507
|
+
const r = safeCall(hostTsWrite, data);
|
|
508
|
+
return typeof r === 'string' ? r : '';
|
|
509
|
+
}
|
|
510
|
+
/** @internal */
|
|
511
|
+
tsReadPayload(data) {
|
|
512
|
+
return hostPayloadToBytes(safeCall(hostTsRead, data));
|
|
513
|
+
}
|
|
514
|
+
/** @internal */
|
|
515
|
+
tsTakePayload(data) {
|
|
516
|
+
return hostPayloadToBytes(safeCall(hostTsTake, data));
|
|
517
|
+
}
|
|
518
|
+
/** @internal */
|
|
519
|
+
tsReadAllPayload(data) {
|
|
520
|
+
return hostPayloadToBytes(safeCall(hostTsReadAll, data));
|
|
521
|
+
}
|
|
522
|
+
// ========================================================================
|
|
523
|
+
// Distributed Locks
|
|
524
|
+
// ========================================================================
|
|
525
|
+
lockAcquire(tenantId, namespace, holderId, lockName, leaseDurationSecs = 30, timeoutMs = 0) {
|
|
526
|
+
return safeCall(hostLockAcquire, tenantId, namespace, holderId, lockName, leaseDurationSecs, BigInt(timeoutMs));
|
|
527
|
+
}
|
|
528
|
+
lockRelease(lockId, tenantId, namespace, holderId, lockVersion) {
|
|
529
|
+
return safeCall(hostLockRelease, lockId, tenantId, namespace, holderId, lockVersion);
|
|
530
|
+
}
|
|
531
|
+
lockRenew(lockId, tenantId, namespace, holderId, lockVersion, leaseDurationSecs = 30) {
|
|
532
|
+
return safeCall(hostLockRenew, lockId, tenantId, namespace, holderId, lockVersion, leaseDurationSecs);
|
|
533
|
+
}
|
|
534
|
+
// ========================================================================
|
|
535
|
+
// Blob Storage
|
|
536
|
+
// ========================================================================
|
|
537
|
+
blobUpload(blobId, data, contentType = 'application/octet-stream') {
|
|
538
|
+
return safeCall(hostBlobUpload, blobId, data, contentType);
|
|
539
|
+
}
|
|
540
|
+
blobDownload(blobId) { return safeCall(hostBlobDownload, blobId); }
|
|
541
|
+
blobDelete(blobId) { return safeCall(hostBlobDelete, blobId); }
|
|
542
|
+
blobList(prefix) { return safeCall(hostBlobList, prefix); }
|
|
543
|
+
// ========================================================================
|
|
544
|
+
// Elastic pool (checkout/checkin)
|
|
545
|
+
// ========================================================================
|
|
546
|
+
/**
|
|
547
|
+
* Checkout an actor from a named pool. Returns handle { actor_id, pool_name, checkout_id } or null on failure.
|
|
548
|
+
*/
|
|
549
|
+
poolCheckout(poolName, timeoutMs = 5000) {
|
|
550
|
+
const result = safeCall(hostPoolCheckout, poolName, BigInt(timeoutMs));
|
|
551
|
+
if (typeof result !== 'string' || result === '' || result.startsWith('ERROR:'))
|
|
552
|
+
return null;
|
|
553
|
+
try {
|
|
554
|
+
return JSON.parse(result);
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Checkin an actor to the pool. Pass actor_id and checkout_id from the handle returned by poolCheckout.
|
|
562
|
+
*/
|
|
563
|
+
poolCheckin(poolName, actorId, checkoutId, healthy) {
|
|
564
|
+
const result = safeCall(hostPoolCheckin, poolName, actorId, checkoutId, healthy);
|
|
565
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
566
|
+
throw new Error(result);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Get pool metrics (total_actors, available_actors, busy_actors, current_load, etc.). Returns null if not available.
|
|
571
|
+
*/
|
|
572
|
+
poolGetMetrics(poolName) {
|
|
573
|
+
const result = safeCall(hostPoolGetMetrics, poolName);
|
|
574
|
+
if (typeof result !== 'string' || result === '' || result.startsWith('ERROR:'))
|
|
575
|
+
return null;
|
|
576
|
+
try {
|
|
577
|
+
return JSON.parse(result);
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
createShardGroup(request) {
|
|
584
|
+
const reqBytes = encodeCreateShardGroupRequest(request);
|
|
585
|
+
const result = safeCall(hostCreateShardGroup, reqBytes);
|
|
586
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
587
|
+
throw new Error(result);
|
|
588
|
+
}
|
|
589
|
+
const bytes = hostPayloadToBytes(result);
|
|
590
|
+
if (bytes.length === 0)
|
|
591
|
+
return { shard_actor_ids: [] };
|
|
592
|
+
const decoded = decodeCreateShardGroupResponse(bytes);
|
|
593
|
+
// Flatten: expose group fields at top level for actor convenience
|
|
594
|
+
const group = decoded.group ?? {};
|
|
595
|
+
return { ...group, ...decoded };
|
|
596
|
+
}
|
|
597
|
+
bulkUpdateShardGroup(request) {
|
|
598
|
+
const result = safeCall(hostBulkUpdateShardGroup, JSON.stringify(request));
|
|
599
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
600
|
+
throw new Error(result);
|
|
601
|
+
}
|
|
602
|
+
return JSON.parse(result);
|
|
603
|
+
}
|
|
604
|
+
mapShardGroup(request) {
|
|
605
|
+
const result = safeCall(hostMapShardGroup, JSON.stringify(request));
|
|
606
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
607
|
+
throw new Error(result);
|
|
608
|
+
}
|
|
609
|
+
return JSON.parse(result);
|
|
610
|
+
}
|
|
611
|
+
scatterGather(request) {
|
|
612
|
+
const reqBytes = encodeScatterGatherRequest(request);
|
|
613
|
+
const result = safeCall(hostScatterGather, reqBytes);
|
|
614
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
615
|
+
throw new Error(result);
|
|
616
|
+
}
|
|
617
|
+
const bytes = hostPayloadToBytes(result);
|
|
618
|
+
if (bytes.length === 0)
|
|
619
|
+
return { shard_responses: [] };
|
|
620
|
+
return decodeScatterGatherResponse(bytes);
|
|
621
|
+
}
|
|
622
|
+
broadcastShardGroup(request) {
|
|
623
|
+
const result = safeCall(hostBroadcastShardGroup, JSON.stringify(request));
|
|
624
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
625
|
+
throw new Error(result);
|
|
626
|
+
}
|
|
627
|
+
return JSON.parse(result);
|
|
628
|
+
}
|
|
629
|
+
reduceShardGroup(request) {
|
|
630
|
+
const result = safeCall(hostReduceShardGroup, JSON.stringify(request));
|
|
631
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
632
|
+
throw new Error(result);
|
|
633
|
+
}
|
|
634
|
+
return JSON.parse(result);
|
|
635
|
+
}
|
|
636
|
+
allReduceShardGroup(request) {
|
|
637
|
+
const result = safeCall(hostAllReduceShardGroup, JSON.stringify(request));
|
|
638
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
639
|
+
throw new Error(result);
|
|
640
|
+
}
|
|
641
|
+
return JSON.parse(result);
|
|
642
|
+
}
|
|
643
|
+
barrierShardGroup(request) {
|
|
644
|
+
const result = safeCall(hostBarrierShardGroup, JSON.stringify(request));
|
|
645
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
646
|
+
throw new Error(result);
|
|
647
|
+
}
|
|
648
|
+
return JSON.parse(result);
|
|
649
|
+
}
|
|
650
|
+
spawnActors(request) {
|
|
651
|
+
const result = safeCall(hostSpawnActors, JSON.stringify(request));
|
|
652
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
653
|
+
throw new Error(result);
|
|
654
|
+
}
|
|
655
|
+
return JSON.parse(result);
|
|
656
|
+
}
|
|
657
|
+
applicationMetricsAdd(applicationId, metrics) {
|
|
658
|
+
const metricsBytes = encodeApplicationMetrics(metrics);
|
|
659
|
+
const result = safeCall(hostApplicationMetricsAdd, applicationId, metricsBytes);
|
|
660
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
661
|
+
throw new Error(result);
|
|
662
|
+
}
|
|
663
|
+
const bytes = hostPayloadToBytes(result);
|
|
664
|
+
if (bytes.length === 0)
|
|
665
|
+
return {};
|
|
666
|
+
try {
|
|
667
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
return {};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
applicationGetMetrics(applicationId, nodeId) {
|
|
674
|
+
const result = safeCall(hostApplicationGetMetrics, applicationId, nodeId);
|
|
675
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
676
|
+
throw new Error(result);
|
|
677
|
+
}
|
|
678
|
+
const bytes = hostPayloadToBytes(result);
|
|
679
|
+
if (bytes.length === 0)
|
|
680
|
+
return {};
|
|
681
|
+
return decodeApplicationMetrics(bytes);
|
|
682
|
+
}
|
|
683
|
+
applicationGetStatus(applicationId, nodeId) {
|
|
684
|
+
const result = safeCall(hostApplicationGetStatus, applicationId, nodeId);
|
|
685
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
686
|
+
throw new Error(result);
|
|
687
|
+
}
|
|
688
|
+
const bytes = hostPayloadToBytes(result);
|
|
689
|
+
if (bytes.length === 0)
|
|
690
|
+
return { node_id: nodeId, node_address: '', application: null };
|
|
691
|
+
return decodeGetApplicationStatusResponse(bytes);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Execute an outbound HTTP request via a named service link.
|
|
695
|
+
*
|
|
696
|
+
* The link must be pre-configured in RuntimeConfig.service_links.
|
|
697
|
+
* The host handles retries, circuit breaking, and auth injection.
|
|
698
|
+
*
|
|
699
|
+
* @param linkName Service link name (e.g. "payments-api")
|
|
700
|
+
* @param method HTTP method ("GET", "POST", "PUT", "DELETE", "PATCH")
|
|
701
|
+
* @param pathAndQuery Path and optional query string (e.g. "/v1/users?limit=10")
|
|
702
|
+
* @param headers Optional extra headers object
|
|
703
|
+
* @param body Optional request body string (JSON or base64-encoded bytes)
|
|
704
|
+
* @returns Response object with status, headers, body
|
|
705
|
+
*/
|
|
706
|
+
httpFetch(linkName, method, pathAndQuery, headers, body) {
|
|
707
|
+
const bodyBytes = body !== undefined && body.length > 0 ? new TextEncoder().encode(body) : new Uint8Array(0);
|
|
708
|
+
const reqWire = encodeHttpFetchRequestWire(headers ?? {}, bodyBytes);
|
|
709
|
+
const result = safeCall(hostHttpFetch, linkName, method, pathAndQuery, reqWire);
|
|
710
|
+
if (typeof result === 'string' && result.startsWith('ERROR:')) {
|
|
711
|
+
throw new Error(result);
|
|
712
|
+
}
|
|
713
|
+
const bytes = hostPayloadToBytes(result);
|
|
714
|
+
if (bytes.length === 0) {
|
|
715
|
+
return { status: 0, headers: {}, body: '' };
|
|
716
|
+
}
|
|
717
|
+
if (hostErrorPrefixBytes(bytes)) {
|
|
718
|
+
throw new Error(new TextDecoder('utf-8', { fatal: false }).decode(bytes));
|
|
719
|
+
}
|
|
720
|
+
const asText = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
|
|
721
|
+
try {
|
|
722
|
+
return JSON.parse(asText);
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
return decodeHttpFetchResponseWire(bytes);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Two-cursor monotonic append-only log backed by KV.
|
|
731
|
+
*
|
|
732
|
+
* Embed in actor state (serialize with `JSON.stringify`). Each consumer
|
|
733
|
+
* tracks its own read cursor so they advance independently.
|
|
734
|
+
*
|
|
735
|
+
* @example
|
|
736
|
+
* ```typescript
|
|
737
|
+
* const log = new EventLog();
|
|
738
|
+
* const seq = log.append(host, 'audit:', { action: 'login' });
|
|
739
|
+
* const [events, newCursor] = log.poll(host, 'audit:', 'consumer-1', 20);
|
|
740
|
+
* ```
|
|
741
|
+
*/
|
|
742
|
+
export class EventLog {
|
|
743
|
+
constructor(watermark = 0) {
|
|
744
|
+
this.watermark = 0;
|
|
745
|
+
this.watermark = watermark;
|
|
746
|
+
}
|
|
747
|
+
/** Append an entry to the log. Returns the assigned sequence number. */
|
|
748
|
+
append(h, prefix, entry) {
|
|
749
|
+
this.watermark++;
|
|
750
|
+
const key = `${prefix}seq:${this.watermark}`;
|
|
751
|
+
try {
|
|
752
|
+
h.kvPutJson(key, entry);
|
|
753
|
+
}
|
|
754
|
+
catch (e) {
|
|
755
|
+
this.watermark--;
|
|
756
|
+
throw new Error(`EventLog.append: ${e}`);
|
|
757
|
+
}
|
|
758
|
+
return this.watermark;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Return up to `limit` events for `consumerId` that arrived after its last cursor.
|
|
762
|
+
* Returns `[events, newCursor]`. The new cursor is persisted in KV.
|
|
763
|
+
*/
|
|
764
|
+
poll(h, prefix, consumerId, limit = 100) {
|
|
765
|
+
const cursorKey = `${prefix}cursor:${consumerId}`;
|
|
766
|
+
const rawCursor = h.kvGet(cursorKey);
|
|
767
|
+
const cursor = rawCursor ? parseInt(rawCursor, 10) || 0 : 0;
|
|
768
|
+
const events = [];
|
|
769
|
+
let newCursor = cursor;
|
|
770
|
+
for (let seq = cursor + 1; seq <= this.watermark && events.length < limit; seq++) {
|
|
771
|
+
const entry = h.kvGetJson(`${prefix}seq:${seq}`);
|
|
772
|
+
if (entry !== null) {
|
|
773
|
+
events.push(entry);
|
|
774
|
+
newCursor = seq;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (newCursor !== cursor) {
|
|
778
|
+
h.kvPut(cursorKey, String(newCursor));
|
|
779
|
+
}
|
|
780
|
+
return [events, newCursor];
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Ergonomic outbound HTTP client backed by a named service link.
|
|
785
|
+
*
|
|
786
|
+
* The link must be pre-configured in RuntimeConfig.service_links.
|
|
787
|
+
* The host handles retries, circuit breaking, and auth injection.
|
|
788
|
+
*
|
|
789
|
+
* @example
|
|
790
|
+
* ```typescript
|
|
791
|
+
* const http = new ServiceHttpClient("payments-api");
|
|
792
|
+
* const balance = http.get("/v1/balance?account=123");
|
|
793
|
+
* const result = http.post("/v1/transfer", { amount: 100 });
|
|
794
|
+
* ```
|
|
795
|
+
*/
|
|
796
|
+
export class ServiceHttpClient {
|
|
797
|
+
constructor(linkName) {
|
|
798
|
+
this.linkName = linkName;
|
|
799
|
+
}
|
|
800
|
+
/** GET request. Returns response object with status, headers, body. */
|
|
801
|
+
get(pathAndQuery, headers) {
|
|
802
|
+
return host.httpFetch(this.linkName, 'GET', pathAndQuery, headers);
|
|
803
|
+
}
|
|
804
|
+
/** POST JSON request. body is serialized to JSON. */
|
|
805
|
+
post(pathAndQuery, body, headers) {
|
|
806
|
+
const bodyStr = body !== undefined ? JSON.stringify(body) : '';
|
|
807
|
+
return host.httpFetch(this.linkName, 'POST', pathAndQuery, headers, bodyStr);
|
|
808
|
+
}
|
|
809
|
+
/** PUT JSON request. */
|
|
810
|
+
put(pathAndQuery, body, headers) {
|
|
811
|
+
const bodyStr = body !== undefined ? JSON.stringify(body) : '';
|
|
812
|
+
return host.httpFetch(this.linkName, 'PUT', pathAndQuery, headers, bodyStr);
|
|
813
|
+
}
|
|
814
|
+
/** DELETE request. */
|
|
815
|
+
delete(pathAndQuery, headers) {
|
|
816
|
+
return host.httpFetch(this.linkName, 'DELETE', pathAndQuery, headers);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/** Global host instance */
|
|
820
|
+
export const host = new Host();
|
|
821
|
+
/** Return the first member of a process group, or null if empty. */
|
|
822
|
+
export function pgFirst(group) {
|
|
823
|
+
return host.processGroups.first(group);
|
|
824
|
+
}
|
|
825
|
+
/** Return the first member of a process group, throwing if empty. */
|
|
826
|
+
export function pgFirstOrThrow(group) {
|
|
827
|
+
return host.processGroups.firstOrThrow(group);
|
|
828
|
+
}
|