@camunda8/orchestration-cluster-api 10.0.0-alpha.38 → 10.0.0-alpha.39

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,10 @@
1
+ # [10.0.0-alpha.39](https://github.com/camunda/orchestration-cluster-api-js/compare/v10.0.0-alpha.38...v10.0.0-alpha.39) (2026-08-27)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **effect:** keep .paginate on the Effect client, as Streams ([#468](https://github.com/camunda/orchestration-cluster-api-js/issues/468)) ([b61cfbe](https://github.com/camunda/orchestration-cluster-api-js/commit/b61cfbe1b324b0a97bb1a77858c53c4de7156e37))
7
+
1
8
  # [10.0.0-alpha.38](https://github.com/camunda/orchestration-cluster-api-js/compare/v10.0.0-alpha.37...v10.0.0-alpha.38) (2026-08-27)
2
9
 
3
10
 
package/README.md CHANGED
@@ -1269,7 +1269,7 @@ first-class [Effect](https://effect.website) surface — a client whose every me
1269
1269
  > resolution — set `"moduleResolution": "bundler"` (or `"node16"`/`"nodenext"`) in your
1270
1270
  > `tsconfig.json`. The Promise-first `.` entry is unaffected.
1271
1271
 
1272
- <!-- snippet-exempt: uses SDK /effect subpath + optional effect peer not available in examples project -->
1272
+ <!-- snippet-source: examples/effect.ts,examples/readme-imports.txt | regions: ReadmeEffectClientImport+ReadmeEffectClient -->
1273
1273
  ```ts
1274
1274
  import { Effect } from 'effect';
1275
1275
  import {
@@ -1326,10 +1326,44 @@ Exports available from `.../effect`:
1326
1326
  the Effect `Clock`, timing out to `EventualConsistencyTimeout`).
1327
1327
  - Dependency injection: `CamundaEffect` (`Context.Service`) + `layer(options?)` (`Layer`) so worker /
1328
1328
  orchestration code composes via `Layer` and swaps a test double trivially.
1329
+ - Pagination: `.paginate(body, opts?)` on every `search*` method, returning an `EffectPaginator`
1330
+ (`pages()` / `items()` → `Stream`, `toArray()` → `Effect`). See below.
1329
1331
 
1330
1332
  **Clock-class win:** `eventually` / `withTimeout` run on the Effect `Clock`, so `TestClock.adjust`
1331
1333
  advances eventual/timeout deterministically in tests — no real-clock burn.
1332
1334
 
1335
+ ### Paginated Search as a `Stream`
1336
+
1337
+ Every `search*` operation on the Effect client carries the same `.paginate` helper the
1338
+ Promise client installs, re-expressed in Effect terms: `pages()` and `items()` are
1339
+ `Stream`s and `toArray()` is an `Effect`. Pages are fetched lazily as they are pulled,
1340
+ and interrupting the fiber cancels the in-flight page request.
1341
+
1342
+ <!-- snippet-source: examples/effect.ts,examples/readme-imports.txt | regions: ReadmeEffectPaginateImport+ReadmeEffectPaginate -->
1343
+ ```ts
1344
+ import { Effect, Stream } from 'effect';
1345
+ import { createCamundaEffectClient } from '@camunda8/orchestration-cluster-api/effect';
1346
+
1347
+ const camunda = createCamundaEffectClient();
1348
+
1349
+ // Walk every ACTIVE process instance, 100 per request, without ever holding more
1350
+ // than one page in memory. `Stream.take` stops pulling — and so stops fetching.
1351
+ const activeKeys = await Effect.runPromise(
1352
+ camunda.searchProcessInstances
1353
+ .paginate({ filter: { state: 'ACTIVE' }, page: { limit: 100 } })
1354
+ .items()
1355
+ .pipe(
1356
+ Stream.map((instance) => instance.processInstanceKey),
1357
+ Stream.take(500),
1358
+ Stream.runCollect
1359
+ )
1360
+ );
1361
+ ```
1362
+
1363
+ Options: `maxPages` (safety cap), `mode` (`auto` | `cursor` | `offset`), and `consistency`
1364
+ (forwarded to the first page only — once paging is under way an empty page is
1365
+ end-of-results, not a stale read).
1366
+
1333
1367
  ### Effect Job Workers
1334
1368
 
1335
1369
  The same subpath also exposes an **Effect-native job worker** — the long-running
@@ -1340,7 +1374,7 @@ and a `TerminalJobError` becomes `throwJobError` (caught by a BPMN error boundar
1340
1374
  uncaught). Success completes the job with the returned variables. It composes over the same
1341
1375
  activation/backpressure runtime the Promise worker uses — it does not reimplement activation.
1342
1376
 
1343
- <!-- snippet-exempt: uses SDK /effect subpath + optional effect peer not available in examples project -->
1377
+ <!-- snippet-source: examples/effect.ts,examples/readme-imports.txt | regions: ReadmeEffectWorkerImport+ReadmeEffectWorker -->
1344
1378
  ```ts
1345
1379
  import { Effect, Schedule } from 'effect';
1346
1380
  import {
@@ -1352,8 +1386,12 @@ import {
1352
1386
 
1353
1387
  const program = Effect.gen(function* () {
1354
1388
  // Forked into the current Scope: interrupted (with a best-effort lease release) when
1355
- // the scope closes.
1356
- yield* createCamundaEffectWorker<{ ok: boolean }>({
1389
+ // the scope closes. Let both type parameters infer — supplying only the completion-
1390
+ // variable type (`createCamundaEffectWorker<{ ok: boolean }>(…)`) makes TypeScript
1391
+ // fall back to the *default* for the handler's requirements (`R = never`) rather
1392
+ // than inferring it, so a handler with dependencies would stop compiling. See
1393
+ // "Injecting Services into a Handler".
1394
+ yield* createCamundaEffectWorker({
1357
1395
  type: 'payment-processing',
1358
1396
  maxJobsToActivate: 10, // activation batch size
1359
1397
  concurrency: 10, // max jobs handled in parallel (backpressure)
@@ -1371,7 +1409,10 @@ const program = Effect.gen(function* () {
1371
1409
  if (yield* isServiceDown()) {
1372
1410
  // Retryable → failJob(retries - 1) with a re-activation backoff.
1373
1411
  return yield* Effect.fail(
1374
- new RetryableJobError({ message: 'downstream unavailable', retryBackoff: '5 seconds' })
1412
+ new RetryableJobError({
1413
+ message: 'downstream unavailable',
1414
+ retryBackoff: '5 seconds',
1415
+ })
1375
1416
  );
1376
1417
  }
1377
1418
  return { ok: true }; // success → completeJob(variables)
@@ -1402,6 +1443,85 @@ Worker exports from `.../effect`:
1402
1443
  `Clock`, so `TestClock.adjust` bounds activation/retry timing in virtual time — the whole loop is
1403
1444
  deterministic in tests, with no real-clock burn.
1404
1445
 
1446
+ ### Injecting Services into a Handler
1447
+
1448
+ A handler is `(job) => Effect.Effect<A, JobError, R>`, and `R` — whatever services the handler
1449
+ depends on — is threaded out through `createCamundaEffectWorker` / `workerLayer` into the worker's
1450
+ own requirements. So a handler's dependencies are provided, and swapped for mocks, exactly like
1451
+ any other `Layer`.
1452
+
1453
+ <!-- snippet-source: examples/effect.ts,examples/readme-imports.txt | regions: ReadmeEffectWorkerServicesImport+ReadmeEffectWorkerServices -->
1454
+ ```ts
1455
+ import { Context, Effect, Layer } from 'effect';
1456
+ import {
1457
+ CamundaEffect,
1458
+ type CamundaEffectClient,
1459
+ layer,
1460
+ workerLayer,
1461
+ } from '@camunda8/orchestration-cluster-api/effect';
1462
+
1463
+ // A service the handler depends on. Nothing about it is Camunda-specific — it is an
1464
+ // ordinary Effect service.
1465
+ class PaymentGateway extends Context.Service<
1466
+ PaymentGateway,
1467
+ { readonly charge: (amount: number) => Effect.Effect<string> }
1468
+ >()('PaymentGateway') {}
1469
+
1470
+ // The handler's requirements flow out through the worker's own requirements, so the
1471
+ // worker layer asks for `PaymentGateway` just like it asks for the Camunda client.
1472
+ const paymentWorker = workerLayer({
1473
+ type: 'payment-processing',
1474
+ handler: (job) =>
1475
+ Effect.gen(function* () {
1476
+ const gateway = yield* PaymentGateway;
1477
+ return { receipt: yield* gateway.charge(Number(job.variables.amount)) };
1478
+ }),
1479
+ });
1480
+ // paymentWorker: Layer<never, never, CamundaEffect | PaymentGateway>
1481
+
1482
+ // Production: the real gateway and a real client.
1483
+ const liveWorker = paymentWorker.pipe(
1484
+ Layer.provide(
1485
+ Layer.succeed(PaymentGateway, {
1486
+ charge: (amount) => Effect.succeed(`live-receipt-${amount}`),
1487
+ })
1488
+ ),
1489
+ Layer.provide(layer())
1490
+ );
1491
+
1492
+ // Tests: the same worker with *both* dependencies swapped. `CamundaEffect` is a service
1493
+ // too, so the broker is mocked exactly like the gateway — the worker runs end-to-end
1494
+ // with neither a payment provider nor a broker.
1495
+ const fakeClient = {
1496
+ activateJobs: () => Effect.succeed({ jobs: [] }),
1497
+ completeJob: () => Effect.void,
1498
+ failJob: () => Effect.void,
1499
+ throwJobError: () => Effect.void,
1500
+ } as unknown as CamundaEffectClient;
1501
+
1502
+ const mockedWorker = paymentWorker.pipe(
1503
+ Layer.provide(Layer.succeed(PaymentGateway, { charge: () => Effect.succeed('mock-receipt') })),
1504
+ Layer.provide(Layer.succeed(CamundaEffect, fakeClient))
1505
+ );
1506
+ ```
1507
+
1508
+ `Layer.succeed(CamundaEffect, fakeClient)` is what the SDK's own worker tests use; see
1509
+ [tests/effect-worker-di.test.ts](tests/effect-worker-di.test.ts) for worked examples that mock both
1510
+ dependencies and drive the loop to a `completeJob` / `failJob` under `TestClock`.
1511
+
1512
+ > **Gotcha — let both type parameters infer.** `createCamundaEffectWorker<A, R>` has `R = never`
1513
+ > as its default, and TypeScript does not infer a type parameter when only *some* are supplied.
1514
+ > So `createCamundaEffectWorker<{ ok: boolean }>({ ... })` pins `R` to `never`, and a handler with
1515
+ > dependencies fails to compile with an error pointing at the handler rather than at the missing
1516
+ > type argument:
1517
+ >
1518
+ > ```
1519
+ > Type 'PaymentGateway' is not assignable to type 'never'.
1520
+ > ```
1521
+ >
1522
+ > Omit both — `A` is inferred from the handler's success value — or supply both
1523
+ > (`createCamundaEffectWorker<{ receipt: string }, PaymentGateway>({ ... })`).
1524
+
1405
1525
  ## Eventual Consistency Polling
1406
1526
 
1407
1527
  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.