@cellaflow/sdk 0.7.0

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.
Files changed (41) hide show
  1. package/dist/cellaflow/v1/common_pb.d.ts +119 -0
  2. package/dist/cellaflow/v1/common_pb.d.ts.map +1 -0
  3. package/dist/cellaflow/v1/common_pb.js +166 -0
  4. package/dist/cellaflow/v1/common_pb.js.map +1 -0
  5. package/dist/cellaflow/v1/idempotency_pb.d.ts +378 -0
  6. package/dist/cellaflow/v1/idempotency_pb.d.ts.map +1 -0
  7. package/dist/cellaflow/v1/idempotency_pb.js +513 -0
  8. package/dist/cellaflow/v1/idempotency_pb.js.map +1 -0
  9. package/dist/cellaflow/v1/internal_pb.d.ts +201 -0
  10. package/dist/cellaflow/v1/internal_pb.d.ts.map +1 -0
  11. package/dist/cellaflow/v1/internal_pb.js +254 -0
  12. package/dist/cellaflow/v1/internal_pb.js.map +1 -0
  13. package/dist/cellaflow/v1/service_connect.d.ts +84 -0
  14. package/dist/cellaflow/v1/service_connect.d.ts.map +1 -0
  15. package/dist/cellaflow/v1/service_connect.js +88 -0
  16. package/dist/cellaflow/v1/service_connect.js.map +1 -0
  17. package/dist/cellaflow/v1/service_pb.d.ts +172 -0
  18. package/dist/cellaflow/v1/service_pb.d.ts.map +1 -0
  19. package/dist/cellaflow/v1/service_pb.js +266 -0
  20. package/dist/cellaflow/v1/service_pb.js.map +1 -0
  21. package/dist/client.d.ts +88 -0
  22. package/dist/client.d.ts.map +1 -0
  23. package/dist/client.js +187 -0
  24. package/dist/client.js.map +1 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +8 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/serialization.d.ts +18 -0
  30. package/dist/serialization.d.ts.map +1 -0
  31. package/dist/serialization.js +41 -0
  32. package/dist/serialization.js.map +1 -0
  33. package/package.json +59 -0
  34. package/src/cellaflow/v1/common_pb.ts +195 -0
  35. package/src/cellaflow/v1/idempotency_pb.ts +591 -0
  36. package/src/cellaflow/v1/internal_pb.ts +308 -0
  37. package/src/cellaflow/v1/service_connect.ts +90 -0
  38. package/src/cellaflow/v1/service_pb.ts +323 -0
  39. package/src/client.ts +277 -0
  40. package/src/index.ts +7 -0
  41. package/src/serialization.ts +53 -0
@@ -0,0 +1,323 @@
1
+ // @generated by protoc-gen-es v1.10.1 with parameter "target=ts,import_extension=.js"
2
+ // @generated from file cellaflow/v1/service.proto (package cellaflow.v1, syntax proto3)
3
+ /* eslint-disable */
4
+ // @ts-nocheck
5
+
6
+ import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
7
+ import { Message, proto3, protoInt64 } from "@bufbuild/protobuf";
8
+ import { StepResult } from "./common_pb.js";
9
+ import { IdempotencyCommitStatus } from "./idempotency_pb.js";
10
+
11
+ /**
12
+ * @generated from message cellaflow.v1.StartSessionRequest
13
+ */
14
+ export class StartSessionRequest extends Message<StartSessionRequest> {
15
+ /**
16
+ * @generated from field: string workflow_id = 1;
17
+ */
18
+ workflowId = "";
19
+
20
+ /**
21
+ * @generated from field: string version = 2;
22
+ */
23
+ version = "";
24
+
25
+ /**
26
+ * Optional client-proposed session identifier.
27
+ *
28
+ * @generated from field: optional string session_id = 3;
29
+ */
30
+ sessionId?: string;
31
+
32
+ constructor(data?: PartialMessage<StartSessionRequest>) {
33
+ super();
34
+ proto3.util.initPartial(data, this);
35
+ }
36
+
37
+ static readonly runtime: typeof proto3 = proto3;
38
+ static readonly typeName = "cellaflow.v1.StartSessionRequest";
39
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
40
+ { no: 1, name: "workflow_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
41
+ { no: 2, name: "version", kind: "scalar", T: 9 /* ScalarType.STRING */ },
42
+ { no: 3, name: "session_id", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
43
+ ]);
44
+
45
+ static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): StartSessionRequest {
46
+ return new StartSessionRequest().fromBinary(bytes, options);
47
+ }
48
+
49
+ static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): StartSessionRequest {
50
+ return new StartSessionRequest().fromJson(jsonValue, options);
51
+ }
52
+
53
+ static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): StartSessionRequest {
54
+ return new StartSessionRequest().fromJsonString(jsonString, options);
55
+ }
56
+
57
+ static equals(a: StartSessionRequest | PlainMessage<StartSessionRequest> | undefined, b: StartSessionRequest | PlainMessage<StartSessionRequest> | undefined): boolean {
58
+ return proto3.util.equals(StartSessionRequest, a, b);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @generated from message cellaflow.v1.StartSessionResponse
64
+ */
65
+ export class StartSessionResponse extends Message<StartSessionResponse> {
66
+ /**
67
+ * @generated from field: string session_id = 1;
68
+ */
69
+ sessionId = "";
70
+
71
+ /**
72
+ * @generated from field: string version = 2;
73
+ */
74
+ version = "";
75
+
76
+ /**
77
+ * @generated from field: bool is_recovered = 3;
78
+ */
79
+ isRecovered = false;
80
+
81
+ constructor(data?: PartialMessage<StartSessionResponse>) {
82
+ super();
83
+ proto3.util.initPartial(data, this);
84
+ }
85
+
86
+ static readonly runtime: typeof proto3 = proto3;
87
+ static readonly typeName = "cellaflow.v1.StartSessionResponse";
88
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
89
+ { no: 1, name: "session_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
90
+ { no: 2, name: "version", kind: "scalar", T: 9 /* ScalarType.STRING */ },
91
+ { no: 3, name: "is_recovered", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
92
+ ]);
93
+
94
+ static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): StartSessionResponse {
95
+ return new StartSessionResponse().fromBinary(bytes, options);
96
+ }
97
+
98
+ static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): StartSessionResponse {
99
+ return new StartSessionResponse().fromJson(jsonValue, options);
100
+ }
101
+
102
+ static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): StartSessionResponse {
103
+ return new StartSessionResponse().fromJsonString(jsonString, options);
104
+ }
105
+
106
+ static equals(a: StartSessionResponse | PlainMessage<StartSessionResponse> | undefined, b: StartSessionResponse | PlainMessage<StartSessionResponse> | undefined): boolean {
107
+ return proto3.util.equals(StartSessionResponse, a, b);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * @generated from message cellaflow.v1.CommitStepRequest
113
+ */
114
+ export class CommitStepRequest extends Message<CommitStepRequest> {
115
+ /**
116
+ * @generated from field: string session_id = 1;
117
+ */
118
+ sessionId = "";
119
+
120
+ /**
121
+ * @generated from field: cellaflow.v1.StepResult step_result = 2;
122
+ */
123
+ stepResult?: StepResult;
124
+
125
+ /**
126
+ * Idempotency lease validation fields
127
+ *
128
+ * @generated from field: optional string idempotency_key = 10;
129
+ */
130
+ idempotencyKey?: string;
131
+
132
+ /**
133
+ * Required whenever idempotency_key is set
134
+ *
135
+ * @generated from field: optional uint64 idempotency_fencing_token = 11;
136
+ */
137
+ idempotencyFencingToken?: bigint;
138
+
139
+ constructor(data?: PartialMessage<CommitStepRequest>) {
140
+ super();
141
+ proto3.util.initPartial(data, this);
142
+ }
143
+
144
+ static readonly runtime: typeof proto3 = proto3;
145
+ static readonly typeName = "cellaflow.v1.CommitStepRequest";
146
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
147
+ { no: 1, name: "session_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
148
+ { no: 2, name: "step_result", kind: "message", T: StepResult },
149
+ { no: 10, name: "idempotency_key", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
150
+ { no: 11, name: "idempotency_fencing_token", kind: "scalar", T: 4 /* ScalarType.UINT64 */, opt: true },
151
+ ]);
152
+
153
+ static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CommitStepRequest {
154
+ return new CommitStepRequest().fromBinary(bytes, options);
155
+ }
156
+
157
+ static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CommitStepRequest {
158
+ return new CommitStepRequest().fromJson(jsonValue, options);
159
+ }
160
+
161
+ static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CommitStepRequest {
162
+ return new CommitStepRequest().fromJsonString(jsonString, options);
163
+ }
164
+
165
+ static equals(a: CommitStepRequest | PlainMessage<CommitStepRequest> | undefined, b: CommitStepRequest | PlainMessage<CommitStepRequest> | undefined): boolean {
166
+ return proto3.util.equals(CommitStepRequest, a, b);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * @generated from message cellaflow.v1.CommitStepResponse
172
+ */
173
+ export class CommitStepResponse extends Message<CommitStepResponse> {
174
+ /**
175
+ * @generated from field: string session_id = 1;
176
+ */
177
+ sessionId = "";
178
+
179
+ /**
180
+ * @generated from field: uint64 next_sequence = 2;
181
+ */
182
+ nextSequence = protoInt64.zero;
183
+
184
+ /**
185
+ * @generated from field: optional cellaflow.v1.IdempotencyCommitStatus idempotency_status = 5;
186
+ */
187
+ idempotencyStatus?: IdempotencyCommitStatus;
188
+
189
+ constructor(data?: PartialMessage<CommitStepResponse>) {
190
+ super();
191
+ proto3.util.initPartial(data, this);
192
+ }
193
+
194
+ static readonly runtime: typeof proto3 = proto3;
195
+ static readonly typeName = "cellaflow.v1.CommitStepResponse";
196
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
197
+ { no: 1, name: "session_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
198
+ { no: 2, name: "next_sequence", kind: "scalar", T: 4 /* ScalarType.UINT64 */ },
199
+ { no: 5, name: "idempotency_status", kind: "enum", T: proto3.getEnumType(IdempotencyCommitStatus), opt: true },
200
+ ]);
201
+
202
+ static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): CommitStepResponse {
203
+ return new CommitStepResponse().fromBinary(bytes, options);
204
+ }
205
+
206
+ static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): CommitStepResponse {
207
+ return new CommitStepResponse().fromJson(jsonValue, options);
208
+ }
209
+
210
+ static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): CommitStepResponse {
211
+ return new CommitStepResponse().fromJsonString(jsonString, options);
212
+ }
213
+
214
+ static equals(a: CommitStepResponse | PlainMessage<CommitStepResponse> | undefined, b: CommitStepResponse | PlainMessage<CommitStepResponse> | undefined): boolean {
215
+ return proto3.util.equals(CommitStepResponse, a, b);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * @generated from message cellaflow.v1.GetGraphRequest
221
+ */
222
+ export class GetGraphRequest extends Message<GetGraphRequest> {
223
+ /**
224
+ * @generated from field: string session_id = 1;
225
+ */
226
+ sessionId = "";
227
+
228
+ /**
229
+ * Maximum number of items to retrieve in a single page.
230
+ * Enforced limits: Default = 100, Max = 1000.
231
+ *
232
+ * @generated from field: optional uint32 limit = 2;
233
+ */
234
+ limit?: number;
235
+
236
+ /**
237
+ * Cursor for pagination token.
238
+ *
239
+ * @generated from field: optional string cursor = 3;
240
+ */
241
+ cursor?: string;
242
+
243
+ constructor(data?: PartialMessage<GetGraphRequest>) {
244
+ super();
245
+ proto3.util.initPartial(data, this);
246
+ }
247
+
248
+ static readonly runtime: typeof proto3 = proto3;
249
+ static readonly typeName = "cellaflow.v1.GetGraphRequest";
250
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
251
+ { no: 1, name: "session_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
252
+ { no: 2, name: "limit", kind: "scalar", T: 13 /* ScalarType.UINT32 */, opt: true },
253
+ { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
254
+ ]);
255
+
256
+ static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetGraphRequest {
257
+ return new GetGraphRequest().fromBinary(bytes, options);
258
+ }
259
+
260
+ static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetGraphRequest {
261
+ return new GetGraphRequest().fromJson(jsonValue, options);
262
+ }
263
+
264
+ static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetGraphRequest {
265
+ return new GetGraphRequest().fromJsonString(jsonString, options);
266
+ }
267
+
268
+ static equals(a: GetGraphRequest | PlainMessage<GetGraphRequest> | undefined, b: GetGraphRequest | PlainMessage<GetGraphRequest> | undefined): boolean {
269
+ return proto3.util.equals(GetGraphRequest, a, b);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * @generated from message cellaflow.v1.GetGraphResponse
275
+ */
276
+ export class GetGraphResponse extends Message<GetGraphResponse> {
277
+ /**
278
+ * @generated from field: string session_id = 1;
279
+ */
280
+ sessionId = "";
281
+
282
+ /**
283
+ * @generated from field: repeated cellaflow.v1.StepResult steps = 2;
284
+ */
285
+ steps: StepResult[] = [];
286
+
287
+ /**
288
+ * Token for fetching the next page, or empty if there are no more pages.
289
+ *
290
+ * @generated from field: optional string next_cursor = 3;
291
+ */
292
+ nextCursor?: string;
293
+
294
+ constructor(data?: PartialMessage<GetGraphResponse>) {
295
+ super();
296
+ proto3.util.initPartial(data, this);
297
+ }
298
+
299
+ static readonly runtime: typeof proto3 = proto3;
300
+ static readonly typeName = "cellaflow.v1.GetGraphResponse";
301
+ static readonly fields: FieldList = proto3.util.newFieldList(() => [
302
+ { no: 1, name: "session_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
303
+ { no: 2, name: "steps", kind: "message", T: StepResult, repeated: true },
304
+ { no: 3, name: "next_cursor", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
305
+ ]);
306
+
307
+ static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): GetGraphResponse {
308
+ return new GetGraphResponse().fromBinary(bytes, options);
309
+ }
310
+
311
+ static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): GetGraphResponse {
312
+ return new GetGraphResponse().fromJson(jsonValue, options);
313
+ }
314
+
315
+ static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): GetGraphResponse {
316
+ return new GetGraphResponse().fromJsonString(jsonString, options);
317
+ }
318
+
319
+ static equals(a: GetGraphResponse | PlainMessage<GetGraphResponse> | undefined, b: GetGraphResponse | PlainMessage<GetGraphResponse> | undefined): boolean {
320
+ return proto3.util.equals(GetGraphResponse, a, b);
321
+ }
322
+ }
323
+
package/src/client.ts ADDED
@@ -0,0 +1,277 @@
1
+ import {
2
+ createPromiseClient,
3
+ PromiseClient,
4
+ Transport,
5
+ } from "@connectrpc/connect";
6
+ import { createGrpcTransport } from "@connectrpc/connect-node";
7
+ import type { PartialMessage } from "@bufbuild/protobuf";
8
+ import { WorkflowEngineService } from "./cellaflow/v1/service_connect.js";
9
+ import type {
10
+ StartSessionResponse,
11
+ CommitStepResponse,
12
+ } from "./cellaflow/v1/service_pb.js";
13
+ import {
14
+ StartSessionRequest,
15
+ CommitStepRequest,
16
+ GetGraphRequest,
17
+ } from "./cellaflow/v1/service_pb.js";
18
+ import { StepStatus } from "./cellaflow/v1/common_pb.js";
19
+ import type {
20
+ CheckCacheResponse,
21
+ RenewLeaseResponse,
22
+ ReleaseLeaseResponse,
23
+ } from "./cellaflow/v1/idempotency_pb.js";
24
+ import {
25
+ CheckCacheRequest,
26
+ RenewLeaseRequest,
27
+ ReleaseLeaseRequest,
28
+ } from "./cellaflow/v1/idempotency_pb.js";
29
+ import { serialize, deserialize } from "./serialization.js";
30
+
31
+ export interface CellaflowClientOptions {
32
+ target?: string;
33
+ secure?: boolean;
34
+ }
35
+
36
+ export class CellaflowClient {
37
+ /**
38
+ * gRPC Client for the Cellaflow Engine (Connect-ES transport).
39
+ * Handles communication with the engine and strictly uses MessagePack
40
+ * for state payloads.
41
+ */
42
+ private client: PromiseClient<typeof WorkflowEngineService>;
43
+
44
+ constructor(options: CellaflowClientOptions = {}) {
45
+ const { target = "localhost:50051", secure = false } = options;
46
+
47
+ // Use http/https based on the secure flag
48
+ const baseUrl = secure ? `https://${target}` : `http://${target}`;
49
+
50
+ const transport: Transport = createGrpcTransport({
51
+ baseUrl,
52
+ httpVersion: "2",
53
+ });
54
+
55
+ this.client = createPromiseClient(WorkflowEngineService, transport);
56
+ }
57
+
58
+ /**
59
+ * Starts a new workflow execution session.
60
+ *
61
+ * @param workflowId - The workflow definition ID.
62
+ * @param version - The workflow version string.
63
+ * @param sessionId - Optional client-proposed session ID. When provided, the
64
+ * engine performs a transactional check-and-insert to prevent concurrent
65
+ * race conditions. Omit to let the engine assign one.
66
+ */
67
+ async startSession(
68
+ workflowId: string,
69
+ version: string,
70
+ sessionId?: string
71
+ ): Promise<StartSessionResponse> {
72
+ const req: PartialMessage<StartSessionRequest> = { workflowId, version };
73
+ // Only set sessionId when truthy — matches Python's `if session_id:` guard.
74
+ // Sending an empty string may trigger the engine's custom-ID validation path.
75
+ if (sessionId) {
76
+ req.sessionId = sessionId;
77
+ }
78
+ return await this.client.startSession(req);
79
+ }
80
+
81
+ /**
82
+ * Commits a completed step result to the session graph.
83
+ *
84
+ * @param sessionId - The session to commit to.
85
+ * @param sequence - The step sequence number (1-based).
86
+ * @param name - Human-readable step name.
87
+ * @param status - The step outcome status.
88
+ * @param outputPayload - Arbitrary step output. Serialized as MessagePack.
89
+ * @param idempotencyKey - Optional idempotency lease key.
90
+ * @param idempotencyFencingToken - Required when `idempotencyKey` is set.
91
+ */
92
+ async commitStep(
93
+ sessionId: string,
94
+ sequence: number,
95
+ name: string,
96
+ status: StepStatus,
97
+ outputPayload: Record<string, any>,
98
+ idempotencyKey?: string,
99
+ idempotencyFencingToken?: number
100
+ ): Promise<CommitStepResponse> {
101
+ // Strictly serialize object to MessagePack
102
+ const serializedState = serialize(outputPayload);
103
+
104
+ const req: PartialMessage<CommitStepRequest> = {
105
+ sessionId,
106
+ stepResult: {
107
+ sequence: BigInt(sequence),
108
+ name,
109
+ status,
110
+ outputPayload: Buffer.from(serializedState) as unknown as Uint8Array<ArrayBuffer>,
111
+ },
112
+ };
113
+
114
+ if (idempotencyKey !== undefined) {
115
+ req.idempotencyKey = idempotencyKey;
116
+ if (idempotencyFencingToken === undefined) {
117
+ throw new Error(
118
+ "idempotencyFencingToken required if idempotencyKey is set"
119
+ );
120
+ }
121
+ req.idempotencyFencingToken = BigInt(idempotencyFencingToken);
122
+ }
123
+
124
+ return await this.client.commitStep(req);
125
+ }
126
+
127
+ /**
128
+ * Returns a paginated list of committed step results for a session.
129
+ *
130
+ * @returns A tuple of (step results, next_cursor). `next_cursor` is
131
+ * `undefined` when there are no more pages. Each step result's
132
+ * `outputPayload` is fully deserialized from MessagePack.
133
+ */
134
+ async getGraph(
135
+ sessionId: string,
136
+ limit?: number,
137
+ cursor?: string
138
+ ): Promise<[Record<string, any>[], string | undefined]> {
139
+ const req: PartialMessage<GetGraphRequest> = { sessionId };
140
+ if (limit !== undefined) {
141
+ req.limit = limit;
142
+ }
143
+ if (cursor !== undefined) {
144
+ req.cursor = cursor;
145
+ }
146
+
147
+ const resp = await this.client.getGraph(req);
148
+
149
+ const results = resp.steps.map((step) => ({
150
+ sequence: step.sequence,
151
+ name: step.name,
152
+ status: step.status,
153
+ // Guard against empty payload (default Uint8Array(0)) to prevent
154
+ // msgpack from throwing on an empty buffer.
155
+ outputPayload:
156
+ step.outputPayload.length > 0 ? deserialize(step.outputPayload) : {},
157
+ idempotencyKey: step.idempotencyKey,
158
+ }));
159
+
160
+ const nextCursor = resp.nextCursor ? resp.nextCursor : undefined;
161
+ return [results, nextCursor];
162
+ }
163
+
164
+ /**
165
+ * Arbitrates the idempotency lease for `idempotencyKey`.
166
+ *
167
+ * Supplying `sessionId` also asks the engine for the session's committed
168
+ * position, returned as `currentSequence` on every status. The idempotency
169
+ * key is opaque to the engine, so the session cannot be inferred from it —
170
+ * without this the engine has nothing to answer from.
171
+ *
172
+ * Supplying `sequence` — the position this caller intends to write to —
173
+ * additionally lets the engine refuse a lease that would authorise a side
174
+ * effect at an already-committed position, instead of rejecting the commit
175
+ * afterwards once the side effect has happened. The engine raises
176
+ * `FAILED_PRECONDITION` when refused. Both fields are optional on the wire;
177
+ * omitting `sequence` keeps the position unguarded.
178
+ */
179
+ async checkIdempotencyCache(
180
+ agentId: string,
181
+ idempotencyKey: string,
182
+ waitTimeoutMs?: number,
183
+ leaseTtlMs?: number,
184
+ sessionId?: string,
185
+ sequence?: number
186
+ ): Promise<CheckCacheResponse> {
187
+ const req: PartialMessage<CheckCacheRequest> = { agentId, idempotencyKey };
188
+ // These proto fields are uint64 → bigint; convert from the JS number API.
189
+ if (waitTimeoutMs !== undefined) {
190
+ req.waitTimeoutMs = BigInt(waitTimeoutMs);
191
+ }
192
+ if (leaseTtlMs !== undefined) {
193
+ req.leaseTtlMs = BigInt(leaseTtlMs);
194
+ }
195
+ if (sessionId !== undefined) {
196
+ req.sessionId = sessionId;
197
+ }
198
+ if (sequence !== undefined) {
199
+ req.sequence = BigInt(sequence);
200
+ }
201
+
202
+ return await this.client.checkIdempotencyCache(req);
203
+ }
204
+
205
+ /**
206
+ * Renews a held idempotency lease.
207
+ *
208
+ * @param timeoutSecs - Bounds the RPC in **seconds**. Heartbeat callers
209
+ * MUST supply a positive value: without a deadline a black-holed
210
+ * connection parks the calling thread indefinitely, and the shutdown
211
+ * path that joins that thread parks with it.
212
+ */
213
+ async renewLease(
214
+ agentId: string,
215
+ idempotencyKey: string,
216
+ fencingToken: number,
217
+ extendMs: number,
218
+ timeoutSecs?: number
219
+ ): Promise<RenewLeaseResponse> {
220
+ const req: PartialMessage<RenewLeaseRequest> = {
221
+ agentId,
222
+ idempotencyKey,
223
+ fencingToken: BigInt(fencingToken),
224
+ extendMs: BigInt(extendMs),
225
+ };
226
+
227
+ return await this.client.renewLease(req, {
228
+ // Guard against timeout=0 which would cause an instant timeout.
229
+ timeoutMs:
230
+ timeoutSecs !== undefined && timeoutSecs > 0
231
+ ? timeoutSecs * 1000
232
+ : undefined,
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Releases a held idempotency lease.
238
+ *
239
+ * @param timeoutSecs - Bounds the RPC in **seconds**.
240
+ */
241
+ async releaseLease(
242
+ agentId: string,
243
+ idempotencyKey: string,
244
+ fencingToken: number,
245
+ reason?: string,
246
+ timeoutSecs?: number
247
+ ): Promise<ReleaseLeaseResponse> {
248
+ const req: PartialMessage<ReleaseLeaseRequest> = {
249
+ agentId,
250
+ idempotencyKey,
251
+ fencingToken: BigInt(fencingToken),
252
+ };
253
+ if (reason !== undefined) {
254
+ req.reason = reason;
255
+ }
256
+
257
+ return await this.client.releaseLease(req, {
258
+ timeoutMs:
259
+ timeoutSecs !== undefined && timeoutSecs > 0
260
+ ? timeoutSecs * 1000
261
+ : undefined,
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Provided for API compatibility with the Python client.
267
+ *
268
+ * Connect-ES v1 with `createGrpcTransport` does not expose an explicit
269
+ * close/shutdown method — HTTP/2 sessions are managed by Node.js and are
270
+ * cleaned up on process exit or when the client is garbage-collected.
271
+ * Long-lived server processes with many short-lived clients should let the
272
+ * garbage collector handle cleanup.
273
+ */
274
+ close(): void {
275
+ // No-op: Connect-ES v1 does not provide a transport.close() API.
276
+ }
277
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./client.js";
2
+ export * from "./serialization.js";
3
+ export * from "./cellaflow/v1/common_pb.js";
4
+ export * from "./cellaflow/v1/idempotency_pb.js";
5
+ // Note: internal_pb is intentionally NOT re-exported here. It contains
6
+ // engine-internal types (CacheRecord, LeaseRecord, etc.) that are not part
7
+ // of the public SDK surface.
@@ -0,0 +1,53 @@
1
+ import { encode, decode } from "@msgpack/msgpack";
2
+
3
+ /**
4
+ * Serializes a JavaScript object to a MessagePack byte array.
5
+ * This ensures we never use JSON for state payloads, mitigating RCE risks.
6
+ *
7
+ * @throws {TypeError} If `data` is not a plain, non-null object.
8
+ */
9
+ export function serialize(data: Record<string, any>): Uint8Array {
10
+ // Explicit null check first — `typeof null === "object"` would otherwise
11
+ // produce a misleading error message reporting "got object" instead of "got null".
12
+ if (data === null) {
13
+ throw new TypeError("Expected an object for serialization, got null");
14
+ }
15
+ if (typeof data !== "object" || Array.isArray(data)) {
16
+ throw new TypeError(
17
+ `Expected an object for serialization, got ${Array.isArray(data) ? "array" : typeof data}`
18
+ );
19
+ }
20
+ return encode(data);
21
+ }
22
+
23
+ /**
24
+ * Deserializes a MessagePack byte array back into a JavaScript object.
25
+ *
26
+ * Uses `ArrayBuffer.isView()` instead of `instanceof Uint8Array` to work
27
+ * correctly across JS realms (worker threads, vm contexts, etc.).
28
+ *
29
+ * @throws {TypeError} If `data` is not a Uint8Array / ArrayBufferView.
30
+ * @throws {TypeError} If the deserialized value is not a plain object.
31
+ */
32
+ export function deserialize(data: Uint8Array): Record<string, any> {
33
+ // ArrayBuffer.isView is cross-realm safe; instanceof Uint8Array is not.
34
+ if (!ArrayBuffer.isView(data)) {
35
+ throw new TypeError(
36
+ `Expected Uint8Array for deserialization, got ${data === null ? "null" : typeof data}`
37
+ );
38
+ }
39
+
40
+ const unpacked = decode(data);
41
+
42
+ if (
43
+ typeof unpacked !== "object" ||
44
+ unpacked === null ||
45
+ Array.isArray(unpacked)
46
+ ) {
47
+ throw new TypeError(
48
+ `Deserialized data is not an object, got ${unpacked === null ? "null" : typeof unpacked}`
49
+ );
50
+ }
51
+
52
+ return unpacked as Record<string, any>;
53
+ }