@nanobpm/urban 0.88.0 → 0.89.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 (30) hide show
  1. package/dist/runtime/core/host.d.ts +105 -1
  2. package/dist/runtime/core/host.js +3 -0
  3. package/dist/runtime/core/modules/mcp.js +68 -0
  4. package/dist/runtime/core/modules/runtime.gen.js +1 -1
  5. package/dist/runtime/engine/nanosdk.d.ts +30 -1
  6. package/dist/runtime/engine/nanosdk.js +163 -1
  7. package/dist/runtime/index.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/src/runtime/browser/runtime.browser.js +77 -1
  10. package/src/runtime/browser/runtime.browser.test.ts +126 -1
  11. package/src/runtime/core/host.ts +111 -0
  12. package/src/runtime/core/modules/actions.test.ts +3 -0
  13. package/src/runtime/core/modules/agent.test.ts +3 -0
  14. package/src/runtime/core/modules/api.test.ts +3 -0
  15. package/src/runtime/core/modules/cancel.test.ts +3 -0
  16. package/src/runtime/core/modules/connectors.test.ts +3 -0
  17. package/src/runtime/core/modules/datasource.migrations.test.ts +3 -0
  18. package/src/runtime/core/modules/instance-tracking.test.ts +3 -0
  19. package/src/runtime/core/modules/mcp.test.ts +97 -0
  20. package/src/runtime/core/modules/mcp.ts +66 -0
  21. package/src/runtime/core/modules/pages.test.ts +3 -0
  22. package/src/runtime/core/modules/runtime.gen.ts +1 -1
  23. package/src/runtime/core/modules/surfaces.test.ts +6 -0
  24. package/src/runtime/core/modules/triggers.test.ts +3 -0
  25. package/src/runtime/core/modules/workers.test.ts +3 -0
  26. package/src/runtime/core/runtime.extensions.test.ts +9 -0
  27. package/src/runtime/core/runtime.test.ts +9 -0
  28. package/src/runtime/engine/nanosdk.test.ts +12 -0
  29. package/src/runtime/engine/nanosdk.ts +194 -1
  30. package/src/runtime/index.ts +4 -0
@@ -38,6 +38,9 @@ function ctxWith(surfaces: Record<string, unknown>): Parameters<typeof mountSurf
38
38
  searchElementInstanceWaitStates: async () => [],
39
39
  getElementInstance: async () => null,
40
40
  searchIncidents: async () => [],
41
+ searchVariables: async () => [],
42
+ searchJobs: async () => [],
43
+ getProcessDefinitionXml: async () => null,
41
44
  resolveIncident: async () => {},
42
45
  updateJobRetries: async () => {},
43
46
  setVariables: async () => {},
@@ -61,6 +64,9 @@ const fakeEngine: EngineClient = {
61
64
  searchElementInstanceWaitStates: async () => [],
62
65
  getElementInstance: async () => null,
63
66
  searchIncidents: async () => [],
67
+ searchVariables: async () => [],
68
+ searchJobs: async () => [],
69
+ getProcessDefinitionXml: async () => null,
64
70
  resolveIncident: async () => {},
65
71
  updateJobRetries: async () => {},
66
72
  setVariables: async () => {},
@@ -39,6 +39,9 @@ class MiniEngine implements EngineClient {
39
39
  async searchElementInstanceWaitStates(): Promise<[]> { return []; }
40
40
  async getElementInstance(): Promise<null> { return null; }
41
41
  async searchIncidents(): Promise<never[]> { return []; }
42
+ async searchVariables(): Promise<never[]> { return []; }
43
+ async searchJobs(): Promise<never[]> { return []; }
44
+ async getProcessDefinitionXml(): Promise<null> { return null; }
42
45
  async resolveIncident(): Promise<void> {}
43
46
  async updateJobRetries(): Promise<void> {}
44
47
  async setVariables(): Promise<void> {}
@@ -36,6 +36,9 @@ class MiniEngine implements EngineClient {
36
36
  async searchElementInstanceWaitStates(): Promise<[]> { return []; }
37
37
  async getElementInstance(): Promise<null> { return null; }
38
38
  async searchIncidents(): Promise<never[]> { return []; }
39
+ async searchVariables(): Promise<never[]> { return []; }
40
+ async searchJobs(): Promise<never[]> { return []; }
41
+ async getProcessDefinitionXml(): Promise<null> { return null; }
39
42
  async resolveIncident(): Promise<void> {}
40
43
  async updateJobRetries(): Promise<void> {}
41
44
  async setVariables(): Promise<void> {}
@@ -44,6 +44,15 @@ class NoopEngine implements EngineClient {
44
44
  async searchIncidents() {
45
45
  return [];
46
46
  }
47
+ async searchVariables() {
48
+ return [];
49
+ }
50
+ async searchJobs() {
51
+ return [];
52
+ }
53
+ async getProcessDefinitionXml() {
54
+ return null;
55
+ }
47
56
  async resolveIncident() {}
48
57
  async updateJobRetries() {}
49
58
  async setVariables() {}
@@ -65,6 +65,15 @@ class FakeEngine implements EngineClient {
65
65
  async searchIncidents() {
66
66
  return [];
67
67
  }
68
+ async searchVariables() {
69
+ return [];
70
+ }
71
+ async searchJobs() {
72
+ return [];
73
+ }
74
+ async getProcessDefinitionXml() {
75
+ return null;
76
+ }
68
77
  async resolveIncident() {}
69
78
  async updateJobRetries() {}
70
79
  async setVariables() {}
@@ -97,6 +97,18 @@ function fakeSdkClient(overrides: Partial<NanoSdkClient> = {}): NanoSdkClient &
97
97
  calls.push("searchIncidents");
98
98
  return { items: [] };
99
99
  },
100
+ async searchVariables() {
101
+ calls.push("searchVariables");
102
+ return { items: [] };
103
+ },
104
+ async searchJobs() {
105
+ calls.push("searchJobs");
106
+ return { items: [] };
107
+ },
108
+ async getProcessDefinitionXml() {
109
+ calls.push("getProcessDefinitionXml");
110
+ return "";
111
+ },
100
112
  async resolveIncident(input) {
101
113
  calls.push("resolveIncident");
102
114
  return { ...input };
@@ -24,12 +24,16 @@ import type {
24
24
  IncidentFilter,
25
25
  IncidentState,
26
26
  IncidentSummary,
27
+ JobFilter,
27
28
  JobHandler,
29
+ JobSummary,
28
30
  ProcessInstanceSnapshot,
29
31
  ProcessInstanceState,
30
32
  UserTaskState,
31
33
  UserTaskSummary,
32
34
  UserTaskFilter,
35
+ VariableFilter,
36
+ VariableSummary,
33
37
  WorkerSubscription,
34
38
  } from "../core/host.ts";
35
39
  import { assertDeployedWaitStateType, isBpmnError } from "../core/host.ts";
@@ -360,6 +364,99 @@ export function mapIncidentRow(
360
364
  };
361
365
  }
362
366
 
367
+ /** Map one engine variable row onto a {@link VariableSummary}, or `undefined` when the row is
368
+ * malformed (missing the `variableKey`, owning `processInstanceKey`, or `name` — the identity a
369
+ * caller needs). `value` is the engine's serialized JSON value (a string); a non-string value is
370
+ * JSON-encoded so the field is always a string a caller can `JSON.parse`. `scopeKey` falls back to
371
+ * the process-instance key for a process-level variable that omits it. `isTruncated` is the
372
+ * engine's large-value-clipped flag, defaulting to `false`. The single source of truth
373
+ * `searchVariables` extracts through. */
374
+ export function mapVariableRow(
375
+ row: Record<string, unknown>,
376
+ log: Log,
377
+ ): VariableSummary | undefined {
378
+ const variableKey = presentEngineKey(row.variableKey);
379
+ if (variableKey === undefined) {
380
+ log("warn", "skipping variable with no variableKey in engine response");
381
+ return undefined;
382
+ }
383
+ const processInstanceKey = presentEngineKey(row.processInstanceKey);
384
+ if (processInstanceKey === undefined) {
385
+ log("warn", "skipping variable with no processInstanceKey in engine response", {
386
+ variableKey,
387
+ });
388
+ return undefined;
389
+ }
390
+ const name = presentText(row.name);
391
+ if (name === undefined) {
392
+ log("warn", "skipping variable with no name in engine response", { variableKey });
393
+ return undefined;
394
+ }
395
+ // A process-level variable is scoped to the process instance itself; the engine may omit
396
+ // `scopeKey` for it, so fall back to the process-instance key rather than dropping the row.
397
+ const scopeKey = presentEngineKey(row.scopeKey) ?? processInstanceKey;
398
+ // The engine serializes `value` as a JSON string. Keep a string as-is; encode any other type
399
+ // (an object/array/number a permissive engine might inline) so the field is always a string the
400
+ // caller can `JSON.parse`.
401
+ const value = typeof row.value === "string" ? row.value : JSON.stringify(row.value ?? null);
402
+ return {
403
+ variableKey,
404
+ name,
405
+ value,
406
+ scopeKey,
407
+ processInstanceKey,
408
+ isTruncated: row.isTruncated === true,
409
+ };
410
+ }
411
+
412
+ /** Map one engine job row onto a {@link JobSummary}, or `undefined` when the row is malformed
413
+ * (missing the `jobKey`, `type`, or owning `processInstanceKey`). `worker`/`retries`/`elementId`/
414
+ * `deadline` are best-effort diagnostics: present when the engine reports them, omitted otherwise
415
+ * — an unset `worker` is the "queued, not leased" signal, so it must be absent rather than `""`.
416
+ * `state` is passed through as a bare string (the engine's job-state enum is broad). The single
417
+ * source of truth `searchJobs` extracts through. */
418
+ export function mapJobRow(
419
+ row: Record<string, unknown>,
420
+ log: Log,
421
+ ): JobSummary | undefined {
422
+ const jobKey = presentEngineKey(row.jobKey);
423
+ if (jobKey === undefined) {
424
+ log("warn", "skipping job with no jobKey in engine response");
425
+ return undefined;
426
+ }
427
+ const type = presentText(row.type);
428
+ if (type === undefined) {
429
+ log("warn", "skipping job with no type in engine response", { jobKey });
430
+ return undefined;
431
+ }
432
+ const processInstanceKey = presentEngineKey(row.processInstanceKey);
433
+ if (processInstanceKey === undefined) {
434
+ log("warn", "skipping job with no processInstanceKey in engine response", { jobKey });
435
+ return undefined;
436
+ }
437
+ const state = presentText(row.state);
438
+ if (state === undefined) {
439
+ log("warn", "skipping job with no state in engine response", { jobKey });
440
+ return undefined;
441
+ }
442
+ const worker = presentText(row.worker);
443
+ const retries = typeof row.retries === "number" && Number.isFinite(row.retries)
444
+ ? row.retries
445
+ : undefined;
446
+ const elementId = presentText(row.elementId);
447
+ const deadline = presentText(row.deadline);
448
+ return {
449
+ jobKey,
450
+ type,
451
+ state,
452
+ processInstanceKey,
453
+ ...(worker ? { worker } : {}),
454
+ ...(retries !== undefined ? { retries } : {}),
455
+ ...(elementId ? { elementId } : {}),
456
+ ...(deadline ? { deadline } : {}),
457
+ };
458
+ }
459
+
363
460
  /** A job as delivered to a nano-sdk job handler: the frame fields plus the
364
461
  * acknowledgement actions the handler must call. */
365
462
  export interface NanoSdkActivatedJob {
@@ -463,6 +560,21 @@ export interface NanoSdkClient {
463
560
  consistency?: unknown,
464
561
  options?: unknown,
465
562
  ): Promise<Record<string, unknown>>;
563
+ searchVariables(
564
+ input: { filter?: Record<string, unknown>; page?: Record<string, unknown> },
565
+ consistency?: unknown,
566
+ options?: unknown,
567
+ ): Promise<Record<string, unknown>>;
568
+ searchJobs(
569
+ input: { filter?: Record<string, unknown>; page?: Record<string, unknown> },
570
+ consistency?: unknown,
571
+ options?: unknown,
572
+ ): Promise<Record<string, unknown>>;
573
+ getProcessDefinitionXml(
574
+ input: { processDefinitionKey: string },
575
+ consistency?: unknown,
576
+ options?: unknown,
577
+ ): Promise<unknown>;
466
578
  resolveIncident(
467
579
  input: { incidentKey: string },
468
580
  options?: unknown,
@@ -680,7 +792,12 @@ export class SdkEngineClient implements EngineClient {
680
792
  });
681
793
  return [];
682
794
  }
683
- return [{ processInstanceKey: String(key), state }];
795
+ const processDefinitionKey = presentEngineKey(it.processDefinitionKey);
796
+ return [{
797
+ processInstanceKey: String(key),
798
+ state,
799
+ ...(processDefinitionKey ? { processDefinitionKey } : {}),
800
+ }];
684
801
  });
685
802
  }
686
803
 
@@ -784,6 +901,82 @@ export class SdkEngineClient implements EngineClient {
784
901
  });
785
902
  }
786
903
 
904
+ async searchVariables(filter?: VariableFilter): Promise<VariableSummary[]> {
905
+ // Same shape as `searchIncidents`: build the engine filter from the transport-agnostic
906
+ // selectors, read at zero-wait consistency (a variable search is eventually consistent), and
907
+ // map each row through the single `mapVariableRow` gate, dropping malformed rows.
908
+ const f: Record<string, unknown> = {};
909
+ const processInstanceKey = presentEngineKey(filter?.processInstanceKey);
910
+ if (processInstanceKey) f.processInstanceKey = processInstanceKey;
911
+ const scopeKey = presentEngineKey(filter?.scopeKey);
912
+ if (scopeKey) f.scopeKey = scopeKey;
913
+ const name = filter?.name?.trim();
914
+ if (name) f.name = name;
915
+ const body = await this.client.searchVariables(
916
+ { filter: f },
917
+ { consistency: { waitUpToMs: 0 } },
918
+ );
919
+ const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
920
+ return items.flatMap((it) => {
921
+ const mapped = mapVariableRow(it, this.log);
922
+ return mapped ? [mapped] : [];
923
+ });
924
+ }
925
+
926
+ async searchJobs(filter?: JobFilter): Promise<JobSummary[]> {
927
+ // Same shape as `searchIncidents`: build the engine filter from the transport-agnostic
928
+ // selectors, read at zero-wait consistency (a job search is eventually consistent), and map
929
+ // each row through the single `mapJobRow` gate, dropping malformed rows.
930
+ const f: Record<string, unknown> = {};
931
+ const processInstanceKey = presentEngineKey(filter?.processInstanceKey);
932
+ if (processInstanceKey) f.processInstanceKey = processInstanceKey;
933
+ const state = filter?.state?.trim();
934
+ if (state) f.state = state;
935
+ const type = filter?.type?.trim();
936
+ if (type) f.type = type;
937
+ const elementId = filter?.elementId?.trim();
938
+ if (elementId) f.elementId = elementId;
939
+ const worker = filter?.worker?.trim();
940
+ if (worker) f.worker = worker;
941
+ const body = await this.client.searchJobs(
942
+ { filter: f },
943
+ { consistency: { waitUpToMs: 0 } },
944
+ );
945
+ const items = Array.isArray(body.items) ? body.items.filter(isRecord) : [];
946
+ return items.flatMap((it) => {
947
+ const mapped = mapJobRow(it, this.log);
948
+ return mapped ? [mapped] : [];
949
+ });
950
+ }
951
+
952
+ async getProcessDefinitionXml(
953
+ processDefinitionKey: string,
954
+ ): Promise<string | null> {
955
+ // A blank key can never address a definition — short-circuit to null rather than issue a
956
+ // `GET /v2/process-definitions//xml` with an empty segment. Normalize a padded-but-valid key so
957
+ // `" 5 "` addresses the same definition, mirroring `getElementInstance`.
958
+ const key = presentEngineKey(processDefinitionKey);
959
+ if (key === undefined) return null;
960
+ let xml: unknown;
961
+ try {
962
+ xml = await this.client.getProcessDefinitionXml(
963
+ { processDefinitionKey: key },
964
+ { consistency: { waitUpToMs: 0 } },
965
+ );
966
+ } catch (err) {
967
+ // A 404 (no such definition) is an expected "not found", not a fault — mirror `getForm`/
968
+ // `getElementInstance`, which treat a failed fetch as absence (null) rather than propagating.
969
+ this.log("warn", "getProcessDefinitionXml: engine fetch failed", {
970
+ processDefinitionKey: key,
971
+ err: err instanceof Error ? err.message : String(err),
972
+ });
973
+ return null;
974
+ }
975
+ // The endpoint returns the raw BPMN XML as a string (204 → empty). Treat a blank/absent body
976
+ // as "no XML" (null) so a caller distinguishes "have it" from "don't".
977
+ return typeof xml === "string" && xml.trim() !== "" ? xml : null;
978
+ }
979
+
787
980
  async resolveIncident(input: { incidentKey: string }): Promise<void> {
788
981
  await this.client.resolveIncident({
789
982
  incidentKey: requireEngineKey(input.incidentKey, "incidentKey"),
@@ -100,13 +100,17 @@ export type {
100
100
  EngineIncidentState,
101
101
  IncidentFilter,
102
102
  IncidentSummary,
103
+ JobFilter,
103
104
  JobHandler,
105
+ JobSummary,
104
106
  ProcessInstanceSnapshot,
105
107
  ProcessInstanceState,
106
108
  SqliteDb,
107
109
  UserTaskState,
108
110
  UserTaskSummary,
109
111
  UserTaskFilter,
112
+ VariableFilter,
113
+ VariableSummary,
110
114
  WatchHandle,
111
115
  WorkerSubscription,
112
116
  } from "./core/host.ts";