@camunda8/orchestration-cluster-api 10.0.0-alpha.30 → 10.0.0-alpha.32

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/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # [10.0.0-alpha.32](https://github.com/camunda/orchestration-cluster-api-js/compare/v10.0.0-alpha.31...v10.0.0-alpha.32) (2026-08-20)
2
+
3
+
4
+ ### Features
5
+
6
+ * **effect:** Effect-native job-worker surface ([#444](https://github.com/camunda/orchestration-cluster-api-js/issues/444)) ([754c096](https://github.com/camunda/orchestration-cluster-api-js/commit/754c09625a930ff6507e7a5f0e6f0a050f80175e)), closes [#437](https://github.com/camunda/orchestration-cluster-api-js/issues/437) [#438](https://github.com/camunda/orchestration-cluster-api-js/issues/438)
7
+
8
+ # [10.0.0-alpha.31](https://github.com/camunda/orchestration-cluster-api-js/compare/v10.0.0-alpha.30...v10.0.0-alpha.31) (2026-08-20)
9
+
10
+
11
+ ### Features
12
+
13
+ * **effect:** first-class Effect v4 surface, clean cut from experimental /fp ([#440](https://github.com/camunda/orchestration-cluster-api-js/issues/440)) ([04d2bd1](https://github.com/camunda/orchestration-cluster-api-js/commit/04d2bd1452e9196fb337deb2ecf50b3c5d179bf4)), closes [#437](https://github.com/camunda/orchestration-cluster-api-js/issues/437) [#437](https://github.com/camunda/orchestration-cluster-api-js/issues/437)
14
+
15
+
16
+ ### BREAKING CHANGES
17
+
18
+ * **effect:** the experimental `/fp` subpath and its `fp-ts` optional peer are
19
+ removed. Migrate to `./effect` and install the optional `effect` (v4) peer; the
20
+ main `.` entry and its Promise-based API are unchanged.
21
+
1
22
  # [10.0.0-alpha.30](https://github.com/camunda/orchestration-cluster-api-js/compare/v10.0.0-alpha.29...v10.0.0-alpha.30) (2026-08-20)
2
23
 
3
24
 
package/README.md CHANGED
@@ -1250,67 +1250,164 @@ Notes:
1250
1250
  - Cancellation classification runs first so aborted fetches are never downgraded to generic network errors.
1251
1251
  - Abort is immediate and idempotent; underlying fetch is signalled.
1252
1252
 
1253
- ## Functional (fp-ts style) Surface (Opt-In Subpath)
1253
+ ## Effect Surface (Opt-In Subpath)
1254
1254
 
1255
- @experimental - this feature is not guaranteed to be tested or stable.
1255
+ The main entry stays Promise-based and pulls in **zero** Effect at runtime. Opt in to a
1256
+ first-class [Effect](https://effect.website) surface — a client whose every method returns an
1257
+ `Effect`, tagged domain errors, and Effect-native combinators — by importing the dedicated
1258
+ `./effect` subpath.
1256
1259
 
1257
- > **Peer dependency:** `fp-ts` is an optional peer dependency. If you use real `fp-ts` functions
1258
- > (e.g. `pipe`, `TE.match`) alongside this subpath, install it separately:
1260
+ > **Peer dependency:** `effect` is an **optional peer dependency** (Effect **v4**). The `./effect`
1261
+ > subpath requires it; install it alongside the SDK:
1259
1262
  > ```sh
1260
- > npm install fp-ts
1263
+ > npm install effect
1261
1264
  > ```
1262
- > The `/fp` subpath works without `fp-ts` installed it exposes structurally-compatible
1263
- > `Either`/`TaskEither` shapes that interoperate with `fp-ts` but do not require it at runtime.
1264
-
1265
- The main entry stays minimal. To opt in to a TaskEither-style facade & helper combinators import from the dedicated subpath:
1265
+ > The main `.` entry never imports `effect`, so Promise-first users are never forced to adopt it.
1266
+ >
1267
+ > **Module resolution:** Effect v4 ships as an `exports`-map-only package (no legacy
1268
+ > `main`/`types`), so consuming the `./effect` types requires a modern TypeScript module
1269
+ > resolution — set `"moduleResolution": "bundler"` (or `"node16"`/`"nodenext"`) in your
1270
+ > `tsconfig.json`. The Promise-first `.` entry is unaffected.
1266
1271
 
1267
- <!-- snippet-exempt: uses SDK /fp subpath not available in examples project -->
1272
+ <!-- snippet-exempt: uses SDK /effect subpath + optional effect peer not available in examples project -->
1268
1273
  ```ts
1274
+ import { Effect } from 'effect';
1269
1275
  import {
1270
- createCamundaFpClient,
1271
- retryTE,
1272
- withTimeoutTE,
1273
- eventuallyTE,
1274
- isLeft,
1275
- } from '@camunda8/orchestration-cluster-api/fp';
1276
-
1277
- const fp = createCamundaFpClient();
1278
- const deployTE = fp.deployResourcesFromFiles(['./bpmn/process.bpmn']);
1279
- const deployed = await deployTE();
1280
- if (isLeft(deployed)) throw deployed.left; // DomainError union
1276
+ createCamundaEffectClient,
1277
+ eventually,
1278
+ EventualConsistencyTimeout,
1279
+ } from '@camunda8/orchestration-cluster-api/effect';
1280
+
1281
+ const camunda = createCamundaEffectClient();
1282
+
1283
+ const program = Effect.gen(function* () {
1284
+ const deployment = yield* camunda.deployResourcesFromFiles(['./bpmn/process.bpmn']);
1285
+ const { processInstanceKey } = yield* camunda.createProcessInstance({
1286
+ processDefinitionKey: deployment.processes[0].processDefinitionKey,
1287
+ });
1288
+ // Poll on the Effect Clock until the instance is searchable, timing out deterministically.
1289
+ // waitUpToMs: 0 asks the SDK for the latest available state without its own wall-clock
1290
+ // wait, so the Effect `eventually` combinator owns the predicate + timeout horizon —
1291
+ // making the eventual-consistency wait deterministic under TestClock.
1292
+ const search = yield* eventually(
1293
+ camunda.searchProcessInstances(
1294
+ { filter: { processInstanceKey } },
1295
+ { consistency: { waitUpToMs: 0 } }
1296
+ ),
1297
+ (s) => s.items.some((i) => i.processInstanceKey === processInstanceKey),
1298
+ { waitUpTo: '30 seconds', interval: '750 millis' }
1299
+ );
1300
+ return { processInstanceKey, search };
1301
+ }).pipe(
1302
+ // Tagged errors → discriminate with catchTag / catchTags instead of a manual switch.
1303
+ Effect.catchTag('EventualConsistencyTimeout', (e: EventualConsistencyTimeout) =>
1304
+ Effect.logError(`Timed out: ${e.message}`).pipe(Effect.andThen(Effect.fail(e)))
1305
+ )
1306
+ );
1281
1307
 
1282
- // Chain with fp-ts (optional) – the returned thunks are structurally compatible with TaskEither
1283
- // import { pipe } from 'fp-ts/function'; import * as TE from 'fp-ts/TaskEither';
1308
+ const result = await Effect.runPromise(program);
1284
1309
  ```
1285
1310
 
1286
1311
  Why a subpath?
1287
1312
 
1288
- - Keeps base bundle lean for the 80% use case.
1289
- - No hard dependency on `fp-ts` at runtime; only structural types.
1290
- - Advanced users can compose with real `fp-ts` without pulling the effect model into the default import path.
1313
+ - Keeps the base bundle lean for the Promise-first 80% use case.
1314
+ - No dependency on `effect` at runtime unless you opt in; it is an **optional** peer.
1315
+ - Unlocks the Effect ecosystem (typed errors, `Schedule`, `Layer`/`Context`, `TestClock`).
1316
+
1317
+ Exports available from `.../effect`:
1318
+
1319
+ - `createCamundaEffectClient(options?)` – a `Proxy` client where every method returns
1320
+ `Effect.Effect<Awaited<R>, DomainError, never>`; the throwing client is reachable via `.inner`.
1321
+ - Tagged errors (`Data.TaggedError`): `CamundaValidationError`, `EventualConsistencyTimeout`,
1322
+ `HttpError`, `CamundaGenericError` — together the `DomainError` union. Discriminate with
1323
+ `Effect.catchTag` / `Effect.catchTags`.
1324
+ - Combinators: `retryWithBackoff` (`Effect.retry` + `Schedule.exponential` + jitter), `withTimeout`
1325
+ (`Effect.timeoutOrElse` with real interruption), `eventually` (a recursive `Effect.sleep` poll on
1326
+ the Effect `Clock`, timing out to `EventualConsistencyTimeout`).
1327
+ - Dependency injection: `CamundaEffect` (`Context.Service`) + `layer(options?)` (`Layer`) so worker /
1328
+ orchestration code composes via `Layer` and swaps a test double trivially.
1329
+
1330
+ **Clock-class win:** `eventually` / `withTimeout` run on the Effect `Clock`, so `TestClock.adjust`
1331
+ advances eventual/timeout deterministically in tests — no real-clock burn.
1332
+
1333
+ ### Effect Job Workers
1334
+
1335
+ The same subpath also exposes an **Effect-native job worker** — the long-running
1336
+ `activateJobs` → handle → `completeJob`/`failJob` loop, modelled as Effect. A handler is
1337
+ `(job) => Effect.Effect<CompleteVars, JobError, R>` with a **typed failure channel**: a
1338
+ `RetryableJobError` becomes `failJob` with `retries - 1` (plus an optional server-side backoff),
1339
+ and a `TerminalJobError` becomes `throwJobError` (caught by a BPMN error boundary, or an incident if
1340
+ uncaught). Success completes the job with the returned variables. It composes over the same
1341
+ activation/backpressure runtime the Promise worker uses — it does not reimplement activation.
1342
+
1343
+ <!-- snippet-exempt: uses SDK /effect subpath + optional effect peer not available in examples project -->
1344
+ ```ts
1345
+ import { Effect, Schedule } from 'effect';
1346
+ import {
1347
+ createCamundaEffectWorker,
1348
+ layer,
1349
+ RetryableJobError,
1350
+ TerminalJobError,
1351
+ } from '@camunda8/orchestration-cluster-api/effect';
1352
+
1353
+ const program = Effect.gen(function* () {
1354
+ // Forked into the current Scope: interrupted (with a best-effort lease release) when
1355
+ // the scope closes.
1356
+ yield* createCamundaEffectWorker<{ ok: boolean }>({
1357
+ type: 'payment-processing',
1358
+ maxJobsToActivate: 10, // activation batch size
1359
+ concurrency: 10, // max jobs handled in parallel (backpressure)
1360
+ pollInterval: '1 second', // between empty polls, on the Effect Clock
1361
+ // Optional: retry the handler in-process on a RetryableJobError before failing the job.
1362
+ handlerRetrySchedule: Schedule.spaced('2 seconds'),
1363
+ handler: (job) =>
1364
+ Effect.gen(function* () {
1365
+ if (!job.variables.amount) {
1366
+ // Terminal → raise a BPMN error / incident.
1367
+ return yield* Effect.fail(
1368
+ new TerminalJobError({ code: 'INVALID_INPUT', message: 'amount is required' })
1369
+ );
1370
+ }
1371
+ if (yield* isServiceDown()) {
1372
+ // Retryable → failJob(retries - 1) with a re-activation backoff.
1373
+ return yield* Effect.fail(
1374
+ new RetryableJobError({ message: 'downstream unavailable', retryBackoff: '5 seconds' })
1375
+ );
1376
+ }
1377
+ return { ok: true }; // success → completeJob(variables)
1378
+ }),
1379
+ });
1380
+
1381
+ // ... the worker runs for the lifetime of this scope.
1382
+ yield* Effect.never;
1383
+ }).pipe(
1384
+ Effect.scoped,
1385
+ Effect.provide(layer()) // provides the `/effect` client the worker depends on
1386
+ );
1291
1387
 
1292
- Exports available from `.../fp`:
1388
+ void program;
1389
+ ```
1293
1390
 
1294
- - `createCamundaFpClient` typed facade (methods return `() => Promise<Either<DomainError,T>>`).
1295
- - Type guards: `isLeft`, `isRight`.
1296
- - Error / type aliases: `DomainError`, `TaskEither`, `Either`, `Left`, `Right`, `Fpify`.
1297
- - Combinators: `retryTE`, `withTimeoutTE`, `eventuallyTE`.
1391
+ Worker exports from `.../effect`:
1298
1392
 
1299
- DomainError union currently includes:
1393
+ - `createCamundaEffectWorker(config)` forks the worker into the current `Scope` and returns a
1394
+ handle (`{ type, join, interrupt }`); provide the client `layer()` as its dependency.
1395
+ - `activateJobsStream(type, options)` – the lower-level `Stream.Stream<Job, DomainError, …>` of
1396
+ activated jobs, polling on the Effect `Clock`.
1397
+ - `workerLayer(config)` – a `Layer` that runs a worker for the layer's lifetime.
1398
+ - Tagged job failures: `RetryableJobError` (→ `failJob`), `TerminalJobError` (→ `throwJobError`),
1399
+ together the `JobError` channel.
1300
1400
 
1301
- - `CamundaValidationError`
1302
- - `EventualConsistencyTimeoutError`
1303
- - HTTP-like error objects (status/body/message) produced by transport
1304
- - Generic `Error`
1305
-
1306
- You can refine left-channel typing later by mapping HTTP status codes or discriminator fields.
1401
+ **Clock-class win:** the activation poll interval and the handler-retry `Schedule` run on the Effect
1402
+ `Clock`, so `TestClock.adjust` bounds activation/retry timing in virtual time — the whole loop is
1403
+ deterministic in tests, with no real-clock burn.
1307
1404
 
1308
1405
  ## Eventual Consistency Polling
1309
1406
 
1310
1407
  Some endpoints accept consistency management options. Pass a `consistency` block (where supported) with `waitUpToMs` and optional `pollIntervalMs` (default 500). If the condition is not met within timeout an `EventualConsistencyTimeoutError` is thrown.
1311
1408
 
1312
1409
  To consume eventual polling in a non‑throwing fashion set the client error mode before invoking an eventually consistent method:
1313
- At present the canonical client operates in throwing mode. Non‑throwing adaptation (Result / fp-ts) is achieved via the functional wrappers rather than mutating the base client.
1410
+ At present the canonical client operates in throwing mode. Non‑throwing adaptation (Result / Effect) is achieved via the functional wrappers rather than mutating the base client.
1314
1411
 
1315
1412
  ### Options
1316
1413
 
@@ -1589,43 +1686,32 @@ When to use:
1589
1686
  - Avoiding try/catch nesting in larger orchestration flows.
1590
1687
  - Converting to libraries expecting an Either/Result pattern.
1591
1688
 
1592
- ### fp-ts Adapter (TaskEither / Either) - EXPERIMENTAL
1593
-
1594
- _Note that this feature is experimental and subject to change._
1689
+ ### Effect Adapter
1595
1690
 
1596
- For projects using `fp-ts`, wrap the throwing client in a lazy `TaskEither` facade:
1691
+ For Effect-based projects, wrap the throwing client in an Effect-flavoured facade whose every method
1692
+ returns an `Effect` with a typed `DomainError` channel:
1597
1693
 
1598
- <!-- snippet-exempt: requires external fp-ts dependency -->
1694
+ <!-- snippet-exempt: requires optional effect peer dependency -->
1599
1695
  ```ts
1600
- import { createCamundaFpClient } from '@camunda8/orchestration-cluster-api/fp';
1601
- import { pipe } from 'fp-ts/function';
1602
- import * as TE from 'fp-ts/TaskEither';
1603
-
1604
- const fp = createCamundaFpClient();
1696
+ import { Effect } from 'effect';
1697
+ import { createCamundaEffectClient } from '@camunda8/orchestration-cluster-api/effect';
1605
1698
 
1606
- const deployTE = fp.createDeployment({ resources: [file] }); // TaskEither<unknown, ExtendedDeploymentResult>
1699
+ const camunda = createCamundaEffectClient();
1607
1700
 
1608
- pipe(
1609
- deployTE(), // invoke the task (returns Promise<Either>)
1610
- (then) => then // typical usage would use TE.match / TE.fold; shown expanded for clarity
1701
+ const deployment = await Effect.runPromise(
1702
+ camunda.createDeployment({ resources: [file] })
1611
1703
  );
1612
-
1613
- // With helpers
1614
- const task = fp.createDeployment({ resources: [file] });
1615
- const either = await task();
1616
- if (either._tag === 'Right') {
1617
- console.log(either.right.deployments.length);
1618
- } else {
1619
- console.error('Error', either.left);
1620
- }
1704
+ console.log(deployment.deployments.length);
1621
1705
  ```
1622
1706
 
1707
+ See [Effect Surface (Opt-In Subpath)](#effect-surface-opt-in-subpath) above for the full surface —
1708
+ tagged errors, `retryWithBackoff` / `withTimeout` / `eventually`, and `Layer`/`Context` DI.
1709
+
1623
1710
  Notes:
1624
1711
 
1625
- - No runtime dependency on `fp-ts`; adapter implements a minimal `Either` shape. Structural typing lets you lift into real `fp-ts` functions (`fromEither`, etc.).
1626
- - Each method becomes a function returning `() => Promise<Either<E,A>>` (a `TaskEither` shape). Invoke it later to execute.
1627
- - Cancellation: calling `.cancel()` on the original promise isn’t surfaced; if you need cancellation use the base client directly.
1628
- - For richer interop, you can map the returned factory to `TE.tryCatch` in userland.
1712
+ - `effect` is an **optional peer dependency**; only the `./effect` subpath imports it.
1713
+ - Each method returns `Effect.Effect<Awaited<R>, DomainError, never>`; the throwing client is reachable via `.inner`.
1714
+ - Failures are narrowed into tagged errors so you discriminate with `Effect.catchTag` / `catchTags`.
1629
1715
 
1630
1716
  ## Pagination
1631
1717
 
@@ -1748,7 +1834,7 @@ Generate an HTML API reference site with TypeDoc (public entry points only):
1748
1834
  npm run docs:api
1749
1835
  ```
1750
1836
 
1751
- Output: static site in `docs/api` (open `docs/api/index.html` in a browser or serve the folder, e.g. `npx http-server docs/api`). Entry points: `src/index.ts`, `src/logger.ts`, `src/fp/index.ts`. Internal generated code, scripts, tests are excluded and private / protected members are filtered. Regenerate after changing public exports.
1837
+ Output: static site in `docs/api` (open `docs/api/index.html` in a browser or serve the folder, e.g. `npx http-server docs/api`). Entry points: `src/index.ts`, `src/logger.ts`, `src/effect/index.ts`. Internal generated code, scripts, tests are excluded and private / protected members are filtered. Regenerate after changing public exports.
1752
1838
 
1753
1839
  ## Contributing
1754
1840