@nanobpm/nano-sdk 1.0.0 → 1.1.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.
package/dist/index.d.cts CHANGED
@@ -1,10 +1,77 @@
1
1
  import { JobActionReceiptSymbol, createCamundaClient as createCamundaClient$1 } from '@camunda8/orchestration-cluster-api';
2
2
  export * from '@camunda8/orchestration-cluster-api';
3
3
 
4
+ /** A job handed to a worker handler. */
5
+ interface EmbeddedJob {
6
+ jobKey: string;
7
+ type: string;
8
+ processInstanceKey: string;
9
+ elementId: string;
10
+ retries: number;
11
+ variables: Record<string, unknown>;
12
+ }
13
+ /** The engine bridge an embedded app supplies (implemented by the app template's
14
+ * Deno host around μ-nano.wasm). Engine-core stays clock-free; the host injects now(). */
15
+ interface EmbeddedHost {
16
+ deploy(xml: string): Promise<{
17
+ processIds: string[];
18
+ }>;
19
+ createInstance(input: {
20
+ processDefinitionId?: string;
21
+ variables?: Record<string, unknown>;
22
+ }): Promise<{
23
+ processInstanceKey: string;
24
+ }>;
25
+ activateJobs(type: string, max: number, timeoutMs: number, worker: string): Promise<EmbeddedJob[]>;
26
+ completeJob(jobKey: string, variables?: Record<string, unknown>): Promise<void>;
27
+ failJob(jobKey: string, retries: number, errorMessage?: string): Promise<void>;
28
+ instanceCompleted(key: string): boolean;
29
+ instanceVariables(key: string): Record<string, unknown>;
30
+ /** Drive timers + dispatch at wall-clock now. Returns true if anything changed. */
31
+ tick(): void;
32
+ }
33
+ /** Pull-based dispatch over an in-process host. Structurally compatible with the
34
+ * FalconTransport surface used by NanoJobWorker + the client proxy. */
35
+ declare class EmbeddedTransport {
36
+ private host;
37
+ private pollMs;
38
+ private subs;
39
+ private timer;
40
+ private closed;
41
+ constructor(host: EmbeddedHost, pollMs?: number);
42
+ createInstance(input: {
43
+ processDefinitionId?: string;
44
+ variables?: Record<string, unknown>;
45
+ awaitCompletion?: boolean;
46
+ }): Promise<{
47
+ status: number;
48
+ body: unknown;
49
+ completion?: {
50
+ processCompleted: boolean;
51
+ variables: unknown;
52
+ processInstanceKey: string;
53
+ };
54
+ }>;
55
+ subscribe(sub: {
56
+ jobType: string;
57
+ worker: string;
58
+ credits: number;
59
+ timeoutMs: number;
60
+ onJob: (j: any) => void;
61
+ }): Promise<void>;
62
+ unsubscribe(jobType: string): void;
63
+ completeJob(jobKey: string, variables?: Record<string, unknown>): void;
64
+ failJob(jobKey: string, retries?: number, errorMessage?: string): void;
65
+ throwError(jobKey: string, _errorCode: string, errorMessage?: string): void;
66
+ grantCredits(jobType: string, n: number): void;
67
+ private pump;
68
+ close(): void;
69
+ }
70
+
4
71
  interface NanoInfo {
5
72
  engine: string;
6
73
  version?: string;
7
- commandStreamPath: string;
74
+ falconPath: string;
8
75
  }
9
76
  /**
10
77
  * Probes a REST address once (cached) and returns the Nano engine info if the
@@ -29,7 +96,19 @@ interface Subscription {
29
96
  fetchVariables: string[] | null;
30
97
  onJob: (job: JobFrame) => void;
31
98
  }
32
- declare class CommandStreamTransport {
99
+ /**
100
+ * Thrown by {@link FalconTransport.createInstance} when the gateway does
101
+ * not acknowledge a create within `submitTimeoutMs`. On the Falcon protocol,
102
+ * admission backpressure is expressed by the server withholding submission
103
+ * credits (no `503`, no retry), so a create otherwise waits indefinitely for
104
+ * intake capacity. This turns that stall into a typed rejection — treat it as
105
+ * "the server is backpressured" and back off; do not tight-loop retry.
106
+ */
107
+ declare class SubmissionTimeoutError extends Error {
108
+ readonly timeoutMs: number;
109
+ constructor(timeoutMs: number);
110
+ }
111
+ declare class FalconTransport {
33
112
  private url;
34
113
  private ws;
35
114
  private open;
@@ -40,13 +119,29 @@ declare class CommandStreamTransport {
40
119
  private heartbeat;
41
120
  private connectPromise;
42
121
  private closed;
43
- constructor(restAddress: string, path: string);
122
+ private defaultSubmitTimeoutMs?;
123
+ /**
124
+ * Server-granted submission-credit window (seeded by `welcome`, topped up by
125
+ * `submissionCredits`). Mirrors the engine's intake metering so creates queue
126
+ * client-side under admission backpressure instead of flooding the gateway.
127
+ */
128
+ private credits;
129
+ private creditWaiters;
130
+ constructor(restAddress: string, path: string, defaultSubmitTimeoutMs?: number);
44
131
  private nextCorr;
45
132
  private send;
46
133
  connect(): Promise<void>;
47
134
  private reconnect;
48
135
  private handle;
49
136
  private sendSubscribe;
137
+ /**
138
+ * Take one submission credit, waiting for the gateway to replenish when the
139
+ * window is exhausted (admission backpressure — no 503, no retry). When
140
+ * `timeoutMs` elapses first, rejects with {@link SubmissionTimeoutError} and
141
+ * removes the queued waiter so no credit slot leaks.
142
+ */
143
+ private acquireCredit;
144
+ private releaseCreditWaiters;
50
145
  /** Create a process instance over the stream. */
51
146
  createInstance(input: {
52
147
  processDefinitionId?: string;
@@ -55,6 +150,13 @@ declare class CommandStreamTransport {
55
150
  awaitCompletion?: boolean;
56
151
  fetchVariables?: string[];
57
152
  requestTimeoutMs?: number;
153
+ /**
154
+ * Client-side bound (ms) on how long to wait for a submission credit before
155
+ * rejecting with {@link SubmissionTimeoutError}. Overrides the transport-wide
156
+ * default. Omit to wait indefinitely under backpressure. Never sent on the
157
+ * wire — the server is unaware of it.
158
+ */
159
+ submitTimeoutMs?: number;
58
160
  }): Promise<{
59
161
  status: number;
60
162
  body: unknown;
@@ -90,9 +192,9 @@ declare class NanoJobWorker {
90
192
  private stopped;
91
193
  readonly jobType: string;
92
194
  readonly name: string;
93
- constructor(transport: CommandStreamTransport, cfg: NanoJobWorkerConfig);
195
+ constructor(transport: FalconTransport, cfg: NanoJobWorkerConfig);
94
196
  /** Bind the transport after async Nano detection. */
95
- bindTransport(transport: CommandStreamTransport): void;
197
+ bindTransport(transport: FalconTransport): void;
96
198
  start(): Promise<void>;
97
199
  private enrich;
98
200
  private dispatch;
@@ -100,20 +202,23 @@ declare class NanoJobWorker {
100
202
  close(): void;
101
203
  }
102
204
 
103
- /** auto: upgrade only on Nano. command-stream: force. rest: never upgrade. */
104
- type NanoTransport = "auto" | "command-stream" | "rest";
205
+ /** auto: upgrade only on Nano. falcon: force. rest: never upgrade. embedded: in-process μ-nano. */
206
+ type NanoTransport = "auto" | "falcon" | "rest" | "embedded";
105
207
  type AnyOpts = Parameters<typeof createCamundaClient$1>[0] & {
106
208
  config?: Record<string, unknown> & {
107
209
  CAMUNDA_TRANSPORT?: NanoTransport;
108
210
  CAMUNDA_REST_ADDRESS?: string;
211
+ CAMUNDA_NANO_SUBMIT_TIMEOUT_MS?: string | number;
109
212
  };
213
+ /** Embedded (ADR 0005) in-process engine host; required when transport is "embedded". */
214
+ embeddedHost?: EmbeddedHost;
110
215
  };
111
216
  /**
112
217
  * Drop-in replacement for the upstream createCamundaClient. Returns the upstream
113
218
  * client wrapped in a Proxy that upgrades createProcessInstance + createJobWorker
114
- * to the command stream when connected to a Nano server (overridable via the
115
- * CAMUNDA_TRANSPORT config: "auto" | "command-stream" | "rest").
219
+ * to the Falcon protocol when connected to a Nano server (overridable via the
220
+ * CAMUNDA_TRANSPORT config: "auto" | "falcon" | "rest").
116
221
  */
117
222
  declare function createCamundaClient(opts?: AnyOpts): ReturnType<typeof createCamundaClient$1>;
118
223
 
119
- export { CommandStreamTransport, type NanoInfo, NanoJobWorker, type NanoTransport, createCamundaClient, createCamundaClient as default, detectNano };
224
+ export { type EmbeddedHost, type EmbeddedJob, EmbeddedTransport, FalconTransport, type NanoInfo, NanoJobWorker, type NanoTransport, SubmissionTimeoutError, createCamundaClient, createCamundaClient as default, detectNano };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,77 @@
1
1
  import { JobActionReceiptSymbol, createCamundaClient as createCamundaClient$1 } from '@camunda8/orchestration-cluster-api';
2
2
  export * from '@camunda8/orchestration-cluster-api';
3
3
 
4
+ /** A job handed to a worker handler. */
5
+ interface EmbeddedJob {
6
+ jobKey: string;
7
+ type: string;
8
+ processInstanceKey: string;
9
+ elementId: string;
10
+ retries: number;
11
+ variables: Record<string, unknown>;
12
+ }
13
+ /** The engine bridge an embedded app supplies (implemented by the app template's
14
+ * Deno host around μ-nano.wasm). Engine-core stays clock-free; the host injects now(). */
15
+ interface EmbeddedHost {
16
+ deploy(xml: string): Promise<{
17
+ processIds: string[];
18
+ }>;
19
+ createInstance(input: {
20
+ processDefinitionId?: string;
21
+ variables?: Record<string, unknown>;
22
+ }): Promise<{
23
+ processInstanceKey: string;
24
+ }>;
25
+ activateJobs(type: string, max: number, timeoutMs: number, worker: string): Promise<EmbeddedJob[]>;
26
+ completeJob(jobKey: string, variables?: Record<string, unknown>): Promise<void>;
27
+ failJob(jobKey: string, retries: number, errorMessage?: string): Promise<void>;
28
+ instanceCompleted(key: string): boolean;
29
+ instanceVariables(key: string): Record<string, unknown>;
30
+ /** Drive timers + dispatch at wall-clock now. Returns true if anything changed. */
31
+ tick(): void;
32
+ }
33
+ /** Pull-based dispatch over an in-process host. Structurally compatible with the
34
+ * FalconTransport surface used by NanoJobWorker + the client proxy. */
35
+ declare class EmbeddedTransport {
36
+ private host;
37
+ private pollMs;
38
+ private subs;
39
+ private timer;
40
+ private closed;
41
+ constructor(host: EmbeddedHost, pollMs?: number);
42
+ createInstance(input: {
43
+ processDefinitionId?: string;
44
+ variables?: Record<string, unknown>;
45
+ awaitCompletion?: boolean;
46
+ }): Promise<{
47
+ status: number;
48
+ body: unknown;
49
+ completion?: {
50
+ processCompleted: boolean;
51
+ variables: unknown;
52
+ processInstanceKey: string;
53
+ };
54
+ }>;
55
+ subscribe(sub: {
56
+ jobType: string;
57
+ worker: string;
58
+ credits: number;
59
+ timeoutMs: number;
60
+ onJob: (j: any) => void;
61
+ }): Promise<void>;
62
+ unsubscribe(jobType: string): void;
63
+ completeJob(jobKey: string, variables?: Record<string, unknown>): void;
64
+ failJob(jobKey: string, retries?: number, errorMessage?: string): void;
65
+ throwError(jobKey: string, _errorCode: string, errorMessage?: string): void;
66
+ grantCredits(jobType: string, n: number): void;
67
+ private pump;
68
+ close(): void;
69
+ }
70
+
4
71
  interface NanoInfo {
5
72
  engine: string;
6
73
  version?: string;
7
- commandStreamPath: string;
74
+ falconPath: string;
8
75
  }
9
76
  /**
10
77
  * Probes a REST address once (cached) and returns the Nano engine info if the
@@ -29,7 +96,19 @@ interface Subscription {
29
96
  fetchVariables: string[] | null;
30
97
  onJob: (job: JobFrame) => void;
31
98
  }
32
- declare class CommandStreamTransport {
99
+ /**
100
+ * Thrown by {@link FalconTransport.createInstance} when the gateway does
101
+ * not acknowledge a create within `submitTimeoutMs`. On the Falcon protocol,
102
+ * admission backpressure is expressed by the server withholding submission
103
+ * credits (no `503`, no retry), so a create otherwise waits indefinitely for
104
+ * intake capacity. This turns that stall into a typed rejection — treat it as
105
+ * "the server is backpressured" and back off; do not tight-loop retry.
106
+ */
107
+ declare class SubmissionTimeoutError extends Error {
108
+ readonly timeoutMs: number;
109
+ constructor(timeoutMs: number);
110
+ }
111
+ declare class FalconTransport {
33
112
  private url;
34
113
  private ws;
35
114
  private open;
@@ -40,13 +119,29 @@ declare class CommandStreamTransport {
40
119
  private heartbeat;
41
120
  private connectPromise;
42
121
  private closed;
43
- constructor(restAddress: string, path: string);
122
+ private defaultSubmitTimeoutMs?;
123
+ /**
124
+ * Server-granted submission-credit window (seeded by `welcome`, topped up by
125
+ * `submissionCredits`). Mirrors the engine's intake metering so creates queue
126
+ * client-side under admission backpressure instead of flooding the gateway.
127
+ */
128
+ private credits;
129
+ private creditWaiters;
130
+ constructor(restAddress: string, path: string, defaultSubmitTimeoutMs?: number);
44
131
  private nextCorr;
45
132
  private send;
46
133
  connect(): Promise<void>;
47
134
  private reconnect;
48
135
  private handle;
49
136
  private sendSubscribe;
137
+ /**
138
+ * Take one submission credit, waiting for the gateway to replenish when the
139
+ * window is exhausted (admission backpressure — no 503, no retry). When
140
+ * `timeoutMs` elapses first, rejects with {@link SubmissionTimeoutError} and
141
+ * removes the queued waiter so no credit slot leaks.
142
+ */
143
+ private acquireCredit;
144
+ private releaseCreditWaiters;
50
145
  /** Create a process instance over the stream. */
51
146
  createInstance(input: {
52
147
  processDefinitionId?: string;
@@ -55,6 +150,13 @@ declare class CommandStreamTransport {
55
150
  awaitCompletion?: boolean;
56
151
  fetchVariables?: string[];
57
152
  requestTimeoutMs?: number;
153
+ /**
154
+ * Client-side bound (ms) on how long to wait for a submission credit before
155
+ * rejecting with {@link SubmissionTimeoutError}. Overrides the transport-wide
156
+ * default. Omit to wait indefinitely under backpressure. Never sent on the
157
+ * wire — the server is unaware of it.
158
+ */
159
+ submitTimeoutMs?: number;
58
160
  }): Promise<{
59
161
  status: number;
60
162
  body: unknown;
@@ -90,9 +192,9 @@ declare class NanoJobWorker {
90
192
  private stopped;
91
193
  readonly jobType: string;
92
194
  readonly name: string;
93
- constructor(transport: CommandStreamTransport, cfg: NanoJobWorkerConfig);
195
+ constructor(transport: FalconTransport, cfg: NanoJobWorkerConfig);
94
196
  /** Bind the transport after async Nano detection. */
95
- bindTransport(transport: CommandStreamTransport): void;
197
+ bindTransport(transport: FalconTransport): void;
96
198
  start(): Promise<void>;
97
199
  private enrich;
98
200
  private dispatch;
@@ -100,20 +202,23 @@ declare class NanoJobWorker {
100
202
  close(): void;
101
203
  }
102
204
 
103
- /** auto: upgrade only on Nano. command-stream: force. rest: never upgrade. */
104
- type NanoTransport = "auto" | "command-stream" | "rest";
205
+ /** auto: upgrade only on Nano. falcon: force. rest: never upgrade. embedded: in-process μ-nano. */
206
+ type NanoTransport = "auto" | "falcon" | "rest" | "embedded";
105
207
  type AnyOpts = Parameters<typeof createCamundaClient$1>[0] & {
106
208
  config?: Record<string, unknown> & {
107
209
  CAMUNDA_TRANSPORT?: NanoTransport;
108
210
  CAMUNDA_REST_ADDRESS?: string;
211
+ CAMUNDA_NANO_SUBMIT_TIMEOUT_MS?: string | number;
109
212
  };
213
+ /** Embedded (ADR 0005) in-process engine host; required when transport is "embedded". */
214
+ embeddedHost?: EmbeddedHost;
110
215
  };
111
216
  /**
112
217
  * Drop-in replacement for the upstream createCamundaClient. Returns the upstream
113
218
  * client wrapped in a Proxy that upgrades createProcessInstance + createJobWorker
114
- * to the command stream when connected to a Nano server (overridable via the
115
- * CAMUNDA_TRANSPORT config: "auto" | "command-stream" | "rest").
219
+ * to the Falcon protocol when connected to a Nano server (overridable via the
220
+ * CAMUNDA_TRANSPORT config: "auto" | "falcon" | "rest").
116
221
  */
117
222
  declare function createCamundaClient(opts?: AnyOpts): ReturnType<typeof createCamundaClient$1>;
118
223
 
119
- export { CommandStreamTransport, type NanoInfo, NanoJobWorker, type NanoTransport, createCamundaClient, createCamundaClient as default, detectNano };
224
+ export { type EmbeddedHost, type EmbeddedJob, EmbeddedTransport, FalconTransport, type NanoInfo, NanoJobWorker, type NanoTransport, SubmissionTimeoutError, createCamundaClient, createCamundaClient as default, detectNano };
package/dist/index.js CHANGED
@@ -19,11 +19,11 @@ async function detectNano(restAddress, fetchImpl = fetch) {
19
19
  if (res.ok) {
20
20
  const body = await res.json().catch(() => ({}));
21
21
  const nano = body?.nano;
22
- if (nano && typeof nano.commandStreamPath === "string") {
22
+ if (nano && typeof nano.falconPath === "string") {
23
23
  info = {
24
24
  engine: String(nano.engine ?? "nanobpmn"),
25
25
  version: nano.version ? String(nano.version) : void 0,
26
- commandStreamPath: nano.commandStreamPath
26
+ falconPath: nano.falconPath
27
27
  };
28
28
  }
29
29
  }
@@ -33,7 +33,7 @@ async function detectNano(restAddress, fetchImpl = fetch) {
33
33
  cache.set(base, info);
34
34
  return info;
35
35
  }
36
- function commandStreamUrl(restAddress, path) {
36
+ function falconUrl(restAddress, path) {
37
37
  let base = normalizeBase(restAddress);
38
38
  if (base.startsWith("https://")) base = "wss://" + base.slice("https://".length);
39
39
  else if (base.startsWith("http://")) base = "ws://" + base.slice("http://".length);
@@ -55,7 +55,17 @@ async function getWebSocket() {
55
55
  }
56
56
 
57
57
  // src/transport.ts
58
- var CommandStreamTransport = class {
58
+ var SubmissionTimeoutError = class extends Error {
59
+ constructor(timeoutMs) {
60
+ super(
61
+ `create submission stalled: no gateway ack within ${timeoutMs}ms (server is applying admission backpressure)`
62
+ );
63
+ this.timeoutMs = timeoutMs;
64
+ this.name = "SubmissionTimeoutError";
65
+ }
66
+ timeoutMs;
67
+ };
68
+ var FalconTransport = class {
59
69
  url;
60
70
  ws = null;
61
71
  open = false;
@@ -66,8 +76,17 @@ var CommandStreamTransport = class {
66
76
  heartbeat = null;
67
77
  connectPromise = null;
68
78
  closed = false;
69
- constructor(restAddress, path) {
70
- this.url = commandStreamUrl(restAddress, path);
79
+ defaultSubmitTimeoutMs;
80
+ /**
81
+ * Server-granted submission-credit window (seeded by `welcome`, topped up by
82
+ * `submissionCredits`). Mirrors the engine's intake metering so creates queue
83
+ * client-side under admission backpressure instead of flooding the gateway.
84
+ */
85
+ credits = 0;
86
+ creditWaiters = [];
87
+ constructor(restAddress, path, defaultSubmitTimeoutMs) {
88
+ this.url = falconUrl(restAddress, path);
89
+ this.defaultSubmitTimeoutMs = defaultSubmitTimeoutMs !== void 0 && defaultSubmitTimeoutMs > 0 ? defaultSubmitTimeoutMs : void 0;
71
90
  }
72
91
  nextCorr() {
73
92
  this.corr += 1;
@@ -95,12 +114,16 @@ var CommandStreamTransport = class {
95
114
  this.handle(f, resolve);
96
115
  };
97
116
  ws.onerror = () => {
98
- if (!this.open) reject(new Error(`command-stream connect failed: ${this.url}`));
117
+ if (!this.open) reject(new Error(`falcon connect failed: ${this.url}`));
99
118
  };
100
119
  ws.onclose = () => {
101
120
  this.open = false;
121
+ this.credits = 0;
122
+ const waiters = this.creditWaiters;
123
+ this.creditWaiters = [];
124
+ for (const w of waiters) w.fail(new Error("falcon closed"));
102
125
  if (this.heartbeat) clearInterval(this.heartbeat);
103
- for (const p of this.pending.values()) p.reject(new Error("command-stream closed"));
126
+ for (const p of this.pending.values()) p.reject(new Error("falcon closed"));
104
127
  this.pending.clear();
105
128
  if (!this.closed) setTimeout(() => void this.reconnect(), 1e3);
106
129
  };
@@ -118,12 +141,14 @@ var CommandStreamTransport = class {
118
141
  switch (f.type) {
119
142
  case "welcome": {
120
143
  this.open = true;
144
+ this.credits = Number(f.submissionCredits ?? 0);
121
145
  const hb = Number(f.heartbeatMs ?? 0);
122
146
  if (hb > 0) {
123
147
  if (this.heartbeat) clearInterval(this.heartbeat);
124
148
  this.heartbeat = setInterval(() => this.send({ type: "heartbeat" }), hb);
125
149
  }
126
150
  for (const sub of this.subs.values()) this.sendSubscribe(sub);
151
+ this.releaseCreditWaiters();
127
152
  resolveConnect();
128
153
  break;
129
154
  }
@@ -155,6 +180,11 @@ var CommandStreamTransport = class {
155
180
  }
156
181
  break;
157
182
  }
183
+ case "submissionCredits": {
184
+ this.credits += Number(f.n ?? 0);
185
+ this.releaseCreditWaiters();
186
+ break;
187
+ }
158
188
  }
159
189
  }
160
190
  sendSubscribe(sub) {
@@ -167,9 +197,51 @@ var CommandStreamTransport = class {
167
197
  fetchVariable: sub.fetchVariables
168
198
  });
169
199
  }
200
+ // ----- submission-credit gating ------------------------------------------
201
+ /**
202
+ * Take one submission credit, waiting for the gateway to replenish when the
203
+ * window is exhausted (admission backpressure — no 503, no retry). When
204
+ * `timeoutMs` elapses first, rejects with {@link SubmissionTimeoutError} and
205
+ * removes the queued waiter so no credit slot leaks.
206
+ */
207
+ acquireCredit(timeoutMs) {
208
+ if (this.credits > 0) {
209
+ this.credits -= 1;
210
+ return Promise.resolve();
211
+ }
212
+ return new Promise((resolve, reject) => {
213
+ let timer;
214
+ const waiter = {
215
+ grant: () => {
216
+ if (timer !== void 0) clearTimeout(timer);
217
+ this.credits -= 1;
218
+ resolve();
219
+ },
220
+ fail: (e) => {
221
+ if (timer !== void 0) clearTimeout(timer);
222
+ reject(e);
223
+ }
224
+ };
225
+ this.creditWaiters.push(waiter);
226
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
227
+ timer = setTimeout(() => {
228
+ const i = this.creditWaiters.indexOf(waiter);
229
+ if (i >= 0) this.creditWaiters.splice(i, 1);
230
+ reject(new SubmissionTimeoutError(timeoutMs));
231
+ }, timeoutMs);
232
+ timer.unref?.();
233
+ }
234
+ });
235
+ }
236
+ releaseCreditWaiters() {
237
+ while (this.credits > 0 && this.creditWaiters.length > 0) {
238
+ this.creditWaiters.shift().grant();
239
+ }
240
+ }
170
241
  /** Create a process instance over the stream. */
171
242
  async createInstance(input) {
172
243
  await this.connect();
244
+ await this.acquireCredit(input.submitTimeoutMs ?? this.defaultSubmitTimeoutMs);
173
245
  const corr = this.nextCorr();
174
246
  let completionResolve = null;
175
247
  const completion = input.awaitCompletion ? new Promise((r) => completionResolve = r) : null;
@@ -221,6 +293,68 @@ var CommandStreamTransport = class {
221
293
  }
222
294
  };
223
295
 
296
+ // src/embedded.ts
297
+ var EmbeddedTransport = class {
298
+ constructor(host, pollMs = 25) {
299
+ this.host = host;
300
+ this.pollMs = pollMs;
301
+ }
302
+ host;
303
+ pollMs;
304
+ subs = /* @__PURE__ */ new Map();
305
+ timer = null;
306
+ closed = false;
307
+ async createInstance(input) {
308
+ const { processInstanceKey } = await this.host.createInstance(input);
309
+ this.host.tick();
310
+ if (!input.awaitCompletion) return { status: 200, body: { processInstanceKey } };
311
+ while (!this.host.instanceCompleted(processInstanceKey)) {
312
+ this.host.tick();
313
+ await new Promise((r) => setTimeout(r, this.pollMs));
314
+ }
315
+ const variables = this.host.instanceVariables(processInstanceKey);
316
+ return { status: 200, body: { processInstanceKey, variables }, completion: { processCompleted: true, variables, processInstanceKey } };
317
+ }
318
+ async subscribe(sub) {
319
+ this.subs.set(sub.jobType, { worker: sub.worker, credits: sub.credits, timeoutMs: sub.timeoutMs, onJob: sub.onJob });
320
+ if (!this.timer) this.timer = setInterval(() => this.pump(), this.pollMs);
321
+ }
322
+ unsubscribe(jobType) {
323
+ this.subs.delete(jobType);
324
+ }
325
+ completeJob(jobKey, variables) {
326
+ void this.host.completeJob(jobKey, variables);
327
+ }
328
+ failJob(jobKey, retries, errorMessage) {
329
+ void this.host.failJob(jobKey, retries ?? 0, errorMessage);
330
+ }
331
+ throwError(jobKey, _errorCode, errorMessage) {
332
+ void this.host.failJob(jobKey, 0, errorMessage);
333
+ }
334
+ grantCredits(jobType, n) {
335
+ const s = this.subs.get(jobType);
336
+ if (s) s.credits += n;
337
+ }
338
+ pump() {
339
+ if (this.closed) return;
340
+ this.host.tick();
341
+ for (const [type, s] of this.subs) {
342
+ if (s.credits <= 0) continue;
343
+ void this.host.activateJobs(type, s.credits, s.timeoutMs, s.worker).then((jobs) => {
344
+ for (const j of jobs) {
345
+ if (s.credits <= 0) break;
346
+ s.credits--;
347
+ s.onJob(j);
348
+ }
349
+ });
350
+ }
351
+ }
352
+ close() {
353
+ this.closed = true;
354
+ if (this.timer) clearInterval(this.timer);
355
+ }
356
+ };
357
+
224
358
  // src/nanoWorker.ts
225
359
  import { JobActionReceiptSymbol as JobActionReceipt } from "@camunda8/orchestration-cluster-api";
226
360
  var NanoJobWorker = class {
@@ -313,6 +447,11 @@ function resolveMode(opts) {
313
447
  const fromEnv = typeof process !== "undefined" ? process.env?.CAMUNDA_TRANSPORT : void 0;
314
448
  return fromOpts ?? fromEnv ?? "auto";
315
449
  }
450
+ function resolveSubmitTimeoutMs(opts) {
451
+ const raw = opts?.config?.CAMUNDA_NANO_SUBMIT_TIMEOUT_MS ?? (typeof process !== "undefined" ? process.env?.CAMUNDA_NANO_SUBMIT_TIMEOUT_MS : void 0);
452
+ const n = typeof raw === "string" ? Number(raw) : raw;
453
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? n : void 0;
454
+ }
316
455
  function baseFrom(restAddress) {
317
456
  return normalizeBase(restAddress).replace(/\/v2$/, "");
318
457
  }
@@ -320,31 +459,40 @@ function createCamundaClient(opts) {
320
459
  const client = createCamundaClientBase(opts);
321
460
  const mode = resolveMode(opts);
322
461
  if (mode === "rest") return client;
462
+ if (mode === "embedded") {
463
+ if (!opts?.embeddedHost) throw new Error("transport 'embedded' requires opts.embeddedHost");
464
+ const transport2 = new EmbeddedTransport(opts.embeddedHost);
465
+ return wrapClient(client, async () => transport2, () => transport2.close());
466
+ }
323
467
  const restAddress = client.getConfig().restAddress;
324
468
  const base = baseFrom(restAddress);
469
+ const submitTimeoutMs = resolveSubmitTimeoutMs(opts);
325
470
  let transport = null;
326
471
  let nano;
327
472
  const ensure = async () => {
328
473
  if (nano === void 0) {
329
- nano = mode === "command-stream" ? { engine: "nanobpmn", commandStreamPath: "/command-stream" } : await detectNano(base);
474
+ nano = mode === "falcon" ? { engine: "nanobpmn", falconPath: "/falcon" } : await detectNano(base);
330
475
  }
331
476
  if (!nano) return null;
332
- if (!transport) transport = new CommandStreamTransport(base, nano.commandStreamPath);
477
+ if (!transport) transport = new FalconTransport(base, nano.falconPath, submitTimeoutMs);
333
478
  return transport;
334
479
  };
480
+ return wrapClient(client, ensure, () => transport?.close());
481
+ }
482
+ function wrapClient(client, ensure, onStop) {
335
483
  return new Proxy(client, {
336
484
  get(target, prop, receiver) {
337
485
  if (prop === "createProcessInstance") {
338
486
  return (input, options) => {
339
- const useStream = ensure();
340
- return useStream.then(async (t) => {
487
+ return ensure().then(async (t) => {
341
488
  if (!t) return target.createProcessInstance(input, options);
342
489
  const r = await t.createInstance({
343
490
  processDefinitionId: input?.processDefinitionId,
344
491
  processDefinitionKey: input?.processDefinitionKey,
345
492
  variables: input?.variables,
346
493
  awaitCompletion: input?.awaitCompletion ?? false,
347
- fetchVariables: input?.fetchVariables
494
+ fetchVariables: input?.fetchVariables,
495
+ submitTimeoutMs: input?.submitTimeoutMs
348
496
  });
349
497
  if (r.status >= 400) throw new Error(`createInstance failed: ${r.status} ${JSON.stringify(r.body)}`);
350
498
  const body = r.body ?? {};
@@ -366,7 +514,7 @@ function createCamundaClient(opts) {
366
514
  }
367
515
  if (prop === "stopAllWorkers") {
368
516
  return () => {
369
- transport?.close();
517
+ onStop?.();
370
518
  return target.stopAllWorkers();
371
519
  };
372
520
  }
@@ -376,8 +524,10 @@ function createCamundaClient(opts) {
376
524
  }
377
525
  var index_default = createCamundaClient;
378
526
  export {
379
- CommandStreamTransport,
527
+ EmbeddedTransport,
528
+ FalconTransport,
380
529
  NanoJobWorker,
530
+ SubmissionTimeoutError,
381
531
  createCamundaClient,
382
532
  index_default as default,
383
533
  detectNano