@nanobpm/urban 0.91.0 → 0.92.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.
@@ -314,16 +314,20 @@ export interface ElementInstanceSummary {
314
314
  * {@link ElementInstanceWaitStateFilter} whose `waitStateType` falls outside that floor is
315
315
  * rejected (the gateway answers HTTP 422); {@link assertDeployedWaitStateType} enforces the
316
316
  * same floor client-side so the divergence surfaces as a clear error offline, in tests, and
317
- * against a live engine alike. Ref: Magikcraft/nano-bpm#1042.
317
+ * against a live engine alike. The floor now covers `JOB | MESSAGE | USER_TASK`
318
+ * (Magikcraft/nano-bpm#1042 shipped `USER_TASK` parks); `TIMER`/`SIGNAL`/`CONDITION` remain
319
+ * the follow-on. Ref: Magikcraft/nano-bpm#1042.
318
320
  */
319
321
  export type WaitStateType = "JOB" | "MESSAGE" | "USER_TASK" | "TIMER" | "SIGNAL" | "CONDITION";
320
322
  /**
321
323
  * The `waitStateType` values the *currently deployed* nanobpmn gateway's wait-state read
322
324
  * model actually implements — the **deployed floor**, a strict subset of the canonical
323
- * {@link WaitStateType} union. The gateway's read model is a snapshot of the Zeebe contract
324
- * taken 2026-06-09 (the same day Zeebe extended it with `USER_TASK`, then `TIMER`/`SIGNAL`/
325
- * `CONDITION`), so it recognizes only `JOB | MESSAGE` and rejects any other filter value
326
- * with HTTP 422.
325
+ * {@link WaitStateType} union. The gateway's read model started as a snapshot of the Zeebe
326
+ * contract taken 2026-06-09 that recognized only `JOB | MESSAGE`; Magikcraft/nano-bpm#1042
327
+ * has since shipped `USER_TASK` parks (added on user-task CREATED, removed on
328
+ * COMPLETED/CANCELED, with the park identity under `details.taskKey`), moving the floor up to
329
+ * `JOB | MESSAGE | USER_TASK`. `TIMER`/`SIGNAL`/`CONDITION` are still rejected with HTTP 422
330
+ * (the 8.10 follow-on tracked in Magikcraft/nano-bpm#1042).
327
331
  *
328
332
  * This constant is the **single source of truth** for that floor: the SDK-backed and
329
333
  * WASM-backed {@link EngineClient} adapters both gate through {@link assertDeployedWaitStateType},
@@ -645,14 +649,14 @@ export interface EngineClient {
645
649
  * (see {@link ElementInstanceWaitState}), so a consumer can read the job/message parks a
646
650
  * {@link searchUserTasks} cannot surface.
647
651
  *
648
- * **Deployed floor.** The currently deployed nanobpmn gateway implements only
649
- * `JOB | MESSAGE` ({@link DEPLOYED_WAIT_STATE_TYPES}) — a `USER_TASK` park is read via
650
- * {@link searchUserTasks}, not here. A `waitStateType` filter outside the floor is rejected
651
- * with {@link UnsupportedWaitStateTypeError} (the live gateway answers HTTP 422); an
652
- * unfiltered search returns only in-floor parks. The `waitStateType` union is
653
- * forward-looking (richer engines may serve more), but this adapter contract pins the
654
- * floor so a query cannot pass in emulation while a real engine rejects it. Ref:
655
- * Magikcraft/nano-bpm#1042.
652
+ * **Deployed floor.** The currently deployed nanobpmn gateway implements
653
+ * `JOB | MESSAGE | USER_TASK` ({@link DEPLOYED_WAIT_STATE_TYPES}); a `USER_TASK` park can be
654
+ * read here (or through {@link searchUserTasks}). A `waitStateType` filter outside the floor
655
+ * (`TIMER`/`SIGNAL`/`CONDITION`) is rejected with {@link UnsupportedWaitStateTypeError} (the
656
+ * live gateway answers HTTP 422); an unfiltered search returns only in-floor parks. The
657
+ * `waitStateType` union is forward-looking (richer engines may serve more), but this adapter
658
+ * contract pins the floor so a query cannot pass in emulation while a real engine rejects it.
659
+ * Ref: Magikcraft/nano-bpm#1042.
656
660
  */
657
661
  searchElementInstanceWaitStates(filter?: ElementInstanceWaitStateFilter): Promise<ElementInstanceWaitState[]>;
658
662
  /**
@@ -45,10 +45,12 @@ export function presentFormIdentifier(value) {
45
45
  /**
46
46
  * The `waitStateType` values the *currently deployed* nanobpmn gateway's wait-state read
47
47
  * model actually implements — the **deployed floor**, a strict subset of the canonical
48
- * {@link WaitStateType} union. The gateway's read model is a snapshot of the Zeebe contract
49
- * taken 2026-06-09 (the same day Zeebe extended it with `USER_TASK`, then `TIMER`/`SIGNAL`/
50
- * `CONDITION`), so it recognizes only `JOB | MESSAGE` and rejects any other filter value
51
- * with HTTP 422.
48
+ * {@link WaitStateType} union. The gateway's read model started as a snapshot of the Zeebe
49
+ * contract taken 2026-06-09 that recognized only `JOB | MESSAGE`; Magikcraft/nano-bpm#1042
50
+ * has since shipped `USER_TASK` parks (added on user-task CREATED, removed on
51
+ * COMPLETED/CANCELED, with the park identity under `details.taskKey`), moving the floor up to
52
+ * `JOB | MESSAGE | USER_TASK`. `TIMER`/`SIGNAL`/`CONDITION` are still rejected with HTTP 422
53
+ * (the 8.10 follow-on tracked in Magikcraft/nano-bpm#1042).
52
54
  *
53
55
  * This constant is the **single source of truth** for that floor: the SDK-backed and
54
56
  * WASM-backed {@link EngineClient} adapters both gate through {@link assertDeployedWaitStateType},
@@ -57,7 +59,7 @@ export function presentFormIdentifier(value) {
57
59
  * backed by the real gateway) only once the engine actually serves the richer type — the
58
60
  * tracking gap is Magikcraft/nano-bpm#1042.
59
61
  */
60
- export const DEPLOYED_WAIT_STATE_TYPES = ["JOB", "MESSAGE"];
62
+ export const DEPLOYED_WAIT_STATE_TYPES = ["JOB", "MESSAGE", "USER_TASK"];
61
63
  /**
62
64
  * Thrown when a wait-state search is asked to filter on a {@link WaitStateType} the deployed
63
65
  * gateway's read model does not implement (outside {@link DEPLOYED_WAIT_STATE_TYPES}). It
@@ -640,9 +640,10 @@ export class SdkEngineClient {
640
640
  }
641
641
  async searchElementInstanceWaitStates(filter) {
642
642
  // The wait-states search surfaces the parks the deployed engine's read model serves.
643
- // A `waitStateType` selector outside the deployed floor (`JOB | MESSAGE`) is rejected by
644
- // the gateway with HTTP 422; fail fast client-side with a clear error instead, so the
645
- // divergence is caught offline and identically to the WASM adapter (No Drift Surfaces).
643
+ // A `waitStateType` selector outside the deployed floor (`JOB | MESSAGE | USER_TASK`) is
644
+ // rejected by the gateway with HTTP 422; fail fast client-side with a clear error instead,
645
+ // so the divergence is caught offline and identically to the WASM adapter (No Drift
646
+ // Surfaces).
646
647
  assertDeployedWaitStateType(filter?.waitStateType);
647
648
  // Same zero-wait read + per-row mapping-gate shape as `searchElementInstances`; the SDK
648
649
  // nests park-specific fields under `details`, which `mapElementInstanceWaitStateRow`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.91.0",
3
+ "version": "0.92.0",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -350,7 +350,9 @@ export interface ElementInstanceSummary {
350
350
  * {@link ElementInstanceWaitStateFilter} whose `waitStateType` falls outside that floor is
351
351
  * rejected (the gateway answers HTTP 422); {@link assertDeployedWaitStateType} enforces the
352
352
  * same floor client-side so the divergence surfaces as a clear error offline, in tests, and
353
- * against a live engine alike. Ref: Magikcraft/nano-bpm#1042.
353
+ * against a live engine alike. The floor now covers `JOB | MESSAGE | USER_TASK`
354
+ * (Magikcraft/nano-bpm#1042 shipped `USER_TASK` parks); `TIMER`/`SIGNAL`/`CONDITION` remain
355
+ * the follow-on. Ref: Magikcraft/nano-bpm#1042.
354
356
  */
355
357
  export type WaitStateType =
356
358
  | "JOB"
@@ -363,10 +365,12 @@ export type WaitStateType =
363
365
  /**
364
366
  * The `waitStateType` values the *currently deployed* nanobpmn gateway's wait-state read
365
367
  * model actually implements — the **deployed floor**, a strict subset of the canonical
366
- * {@link WaitStateType} union. The gateway's read model is a snapshot of the Zeebe contract
367
- * taken 2026-06-09 (the same day Zeebe extended it with `USER_TASK`, then `TIMER`/`SIGNAL`/
368
- * `CONDITION`), so it recognizes only `JOB | MESSAGE` and rejects any other filter value
369
- * with HTTP 422.
368
+ * {@link WaitStateType} union. The gateway's read model started as a snapshot of the Zeebe
369
+ * contract taken 2026-06-09 that recognized only `JOB | MESSAGE`; Magikcraft/nano-bpm#1042
370
+ * has since shipped `USER_TASK` parks (added on user-task CREATED, removed on
371
+ * COMPLETED/CANCELED, with the park identity under `details.taskKey`), moving the floor up to
372
+ * `JOB | MESSAGE | USER_TASK`. `TIMER`/`SIGNAL`/`CONDITION` are still rejected with HTTP 422
373
+ * (the 8.10 follow-on tracked in Magikcraft/nano-bpm#1042).
370
374
  *
371
375
  * This constant is the **single source of truth** for that floor: the SDK-backed and
372
376
  * WASM-backed {@link EngineClient} adapters both gate through {@link assertDeployedWaitStateType},
@@ -375,7 +379,7 @@ export type WaitStateType =
375
379
  * backed by the real gateway) only once the engine actually serves the richer type — the
376
380
  * tracking gap is Magikcraft/nano-bpm#1042.
377
381
  */
378
- export const DEPLOYED_WAIT_STATE_TYPES: readonly WaitStateType[] = ["JOB", "MESSAGE"];
382
+ export const DEPLOYED_WAIT_STATE_TYPES: readonly WaitStateType[] = ["JOB", "MESSAGE", "USER_TASK"];
379
383
 
380
384
  /**
381
385
  * Thrown when a wait-state search is asked to filter on a {@link WaitStateType} the deployed
@@ -701,14 +705,14 @@ export interface EngineClient {
701
705
  * (see {@link ElementInstanceWaitState}), so a consumer can read the job/message parks a
702
706
  * {@link searchUserTasks} cannot surface.
703
707
  *
704
- * **Deployed floor.** The currently deployed nanobpmn gateway implements only
705
- * `JOB | MESSAGE` ({@link DEPLOYED_WAIT_STATE_TYPES}) — a `USER_TASK` park is read via
706
- * {@link searchUserTasks}, not here. A `waitStateType` filter outside the floor is rejected
707
- * with {@link UnsupportedWaitStateTypeError} (the live gateway answers HTTP 422); an
708
- * unfiltered search returns only in-floor parks. The `waitStateType` union is
709
- * forward-looking (richer engines may serve more), but this adapter contract pins the
710
- * floor so a query cannot pass in emulation while a real engine rejects it. Ref:
711
- * Magikcraft/nano-bpm#1042.
708
+ * **Deployed floor.** The currently deployed nanobpmn gateway implements
709
+ * `JOB | MESSAGE | USER_TASK` ({@link DEPLOYED_WAIT_STATE_TYPES}); a `USER_TASK` park can be
710
+ * read here (or through {@link searchUserTasks}). A `waitStateType` filter outside the floor
711
+ * (`TIMER`/`SIGNAL`/`CONDITION`) is rejected with {@link UnsupportedWaitStateTypeError} (the
712
+ * live gateway answers HTTP 422); an unfiltered search returns only in-floor parks. The
713
+ * `waitStateType` union is forward-looking (richer engines may serve more), but this adapter
714
+ * contract pins the floor so a query cannot pass in emulation while a real engine rejects it.
715
+ * Ref: Magikcraft/nano-bpm#1042.
712
716
  */
713
717
  searchElementInstanceWaitStates(
714
718
  filter?: ElementInstanceWaitStateFilter,
@@ -640,14 +640,16 @@ test("normalizeWaitStateType maps the wait-state kinds (case-insensitive) and re
640
640
  });
641
641
 
642
642
  test("assertDeployedWaitStateType: allows the floor and an absent selector, rejects the rest", () => {
643
- // The deployed floor is exactly JOB|MESSAGE — the single source of truth both adapters gate on.
644
- assert.deepEqual([...DEPLOYED_WAIT_STATE_TYPES], ["JOB", "MESSAGE"]);
643
+ // The deployed floor is exactly JOB|MESSAGE|USER_TASK — the single source of truth both
644
+ // adapters gate on (USER_TASK shipped in Magikcraft/nano-bpm#1042).
645
+ assert.deepEqual([...DEPLOYED_WAIT_STATE_TYPES], ["JOB", "MESSAGE", "USER_TASK"]);
645
646
  // In-floor and absent selectors are no-ops (no throw).
646
647
  assert.doesNotThrow(() => assertDeployedWaitStateType(undefined));
647
648
  assert.doesNotThrow(() => assertDeployedWaitStateType("JOB"));
648
649
  assert.doesNotThrow(() => assertDeployedWaitStateType("MESSAGE"));
649
- // Every canonical type outside the floor is rejected with a typed, descriptive error.
650
- for (const waitStateType of ["USER_TASK", "TIMER", "SIGNAL", "CONDITION"] as const) {
650
+ assert.doesNotThrow(() => assertDeployedWaitStateType("USER_TASK"));
651
+ // Every canonical type still outside the floor is rejected with a typed, descriptive error.
652
+ for (const waitStateType of ["TIMER", "SIGNAL", "CONDITION"] as const) {
651
653
  assert.throws(
652
654
  () => assertDeployedWaitStateType(waitStateType),
653
655
  (err: unknown) => {
@@ -843,11 +845,56 @@ test("searchElementInstanceWaitStates forwards a waitStateType selector and read
843
845
  ]);
844
846
  });
845
847
 
848
+ test("searchElementInstanceWaitStates maps a USER_TASK park, unwrapping details.taskKey → userTaskKey (mock SDK client, not a live gateway)", async () => {
849
+ // MOCK-ONLY LEG: this exercises the SDK adapter's row mapping against a `fakeSdkClient`, not
850
+ // a live gateway — the canonical parity assertion against a real engine lives in the
851
+ // adapter-agnostic `runEngineClientContract` USER_TASK leg (contract.ts), run against the
852
+ // live `SdkEngineClient` in the integration lane. Here we only pin the shape the engine's
853
+ // wait-state read model returns for a USER_TASK park (Magikcraft/nano-bpm#1042): the park's
854
+ // `taskKey` under `details` unwraps to the seam's `userTaskKey`.
855
+ let seenInput: unknown;
856
+ const client = fakeSdkClient({
857
+ searchElementInstanceWaitStates: async (input) => {
858
+ seenInput = input;
859
+ return {
860
+ items: [
861
+ {
862
+ elementInstanceKey: 7,
863
+ processInstanceKey: 3,
864
+ elementId: "review",
865
+ type: "USER_TASK",
866
+ details: { waitStateType: "USER_TASK", taskKey: 90 },
867
+ },
868
+ {
869
+ elementInstanceKey: 8,
870
+ processInstanceKey: 3,
871
+ elementId: "review2",
872
+ details: { waitStateType: "USER_TASK" },
873
+ }, // USER_TASK with no taskKey → dropped (missing required identity)
874
+ ],
875
+ };
876
+ },
877
+ });
878
+ const engine = new SdkEngineClient(client);
879
+ const out = await engine.searchElementInstanceWaitStates({ processInstanceKey: "3", waitStateType: "USER_TASK" });
880
+ assert.deepEqual(seenInput, { filter: { processInstanceKey: "3", waitStateType: "USER_TASK" } });
881
+ assert.deepEqual(out, [
882
+ {
883
+ elementInstanceKey: "7",
884
+ processInstanceKey: "3",
885
+ elementId: "review",
886
+ elementType: "USER_TASK",
887
+ waitStateType: "USER_TASK",
888
+ userTaskKey: "90",
889
+ },
890
+ ]);
891
+ });
892
+
846
893
  test("searchElementInstanceWaitStates rejects a waitStateType outside the deployed floor without calling the gateway", async () => {
847
- // The deployed gateway's read model implements only JOB|MESSAGE and 422s any other filter
848
- // value. The SDK adapter must fail fast client-side (not issue the call), so an app authoring
849
- // `waitStateType: "USER_TASK"` cannot ship green then blow up at runtime (issue
850
- // nanobpm/nano-ide#497).
894
+ // The deployed gateway's read model implements JOB|MESSAGE|USER_TASK and 422s any other
895
+ // filter value. The SDK adapter must fail fast client-side (not issue the call), so an app
896
+ // authoring `waitStateType: "TIMER"` cannot ship green then blow up at runtime (issues
897
+ // nanobpm/nano-ide#497, #498).
851
898
  let called = false;
852
899
  const client = fakeSdkClient({
853
900
  searchElementInstanceWaitStates: async () => {
@@ -856,7 +903,7 @@ test("searchElementInstanceWaitStates rejects a waitStateType outside the deploy
856
903
  },
857
904
  });
858
905
  const engine = new SdkEngineClient(client);
859
- for (const waitStateType of ["USER_TASK", "TIMER", "SIGNAL", "CONDITION"] as const) {
906
+ for (const waitStateType of ["TIMER", "SIGNAL", "CONDITION"] as const) {
860
907
  await assert.rejects(
861
908
  engine.searchElementInstanceWaitStates({ waitStateType }),
862
909
  (err: unknown) => {
@@ -868,8 +915,9 @@ test("searchElementInstanceWaitStates rejects a waitStateType outside the deploy
868
915
  );
869
916
  }
870
917
  assert.equal(called, false, "the gateway must not be called for an out-of-floor filter");
871
- // An in-floor filter still reaches the gateway.
918
+ // Every in-floor filter still reaches the gateway.
872
919
  await engine.searchElementInstanceWaitStates({ waitStateType: "JOB" });
920
+ await engine.searchElementInstanceWaitStates({ waitStateType: "USER_TASK" });
873
921
  assert.equal(called, true);
874
922
  });
875
923
 
@@ -867,9 +867,10 @@ export class SdkEngineClient implements EngineClient {
867
867
  filter?: ElementInstanceWaitStateFilter,
868
868
  ): Promise<ElementInstanceWaitState[]> {
869
869
  // The wait-states search surfaces the parks the deployed engine's read model serves.
870
- // A `waitStateType` selector outside the deployed floor (`JOB | MESSAGE`) is rejected by
871
- // the gateway with HTTP 422; fail fast client-side with a clear error instead, so the
872
- // divergence is caught offline and identically to the WASM adapter (No Drift Surfaces).
870
+ // A `waitStateType` selector outside the deployed floor (`JOB | MESSAGE | USER_TASK`) is
871
+ // rejected by the gateway with HTTP 422; fail fast client-side with a clear error instead,
872
+ // so the divergence is caught offline and identically to the WASM adapter (No Drift
873
+ // Surfaces).
873
874
  assertDeployedWaitStateType(filter?.waitStateType);
874
875
  // Same zero-wait read + per-row mapping-gate shape as `searchElementInstances`; the SDK
875
876
  // nests park-specific fields under `details`, which `mapElementInstanceWaitStateRow`