@5minds/processcube_app_sdk 8.2.3 → 8.3.0-develop-31e112-mnj9lsts

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/README.md CHANGED
@@ -46,6 +46,415 @@ Es können nur Komponenten und Funktionen importiert werden, die im Browser funk
46
46
  import { DynamicLink } from '@5minds/processcube_app_sdk/client';
47
47
  ```
48
48
 
49
+ ### External Tasks
50
+
51
+ External Tasks ermöglichen es, eigene Geschäftslogik in einer Next.js App auszuführen, die von der ProcessCube Engine als Aufgabe vergeben wird. Erreicht ein BPMN-Prozess einen External Service Task, veröffentlicht die Engine diesen unter einem **Topic**. Ein passender Worker in der App holt sich den Task ab, verarbeitet ihn und gibt das Ergebnis zurück.
52
+
53
+ Das App SDK übernimmt dabei die komplette Infrastruktur: Worker-Prozesse werden automatisch gestartet, überwacht und bei Fehlern neu gestartet. Der Entwickler schreibt nur die eigentliche Handler-Funktion.
54
+
55
+ #### Architektur
56
+
57
+ Das folgende Diagramm zeigt die drei beteiligten Schichten und ihre Kommunikation:
58
+
59
+ ```mermaid
60
+ graph LR
61
+ subgraph ProcessCube Engine
62
+ E[Engine]
63
+ end
64
+
65
+ subgraph Next.js App – Hauptprozess
66
+ A[ExternalTaskAdapter]
67
+ W[File Watcher]
68
+ T[Token Management]
69
+ end
70
+
71
+ subgraph Eigener Node.js Prozess je Topic
72
+ WP1[Worker Process<br/>Topic: order/process]
73
+ WP2[Worker Process<br/>Topic: invoice/send]
74
+ end
75
+
76
+ E -- "HTTP: Fetch & Lock /<br/>Finish / Error /<br/>Extend Lock" --> WP1
77
+ E -- "HTTP: Fetch & Lock /<br/>Finish / Error /<br/>Extend Lock" --> WP2
78
+
79
+ A -- "IPC: create / restart /<br/>updateIdentity" --> WP1
80
+ A -- "IPC: create / restart /<br/>updateIdentity" --> WP2
81
+
82
+ W -- "Dateiänderung erkannt" --> A
83
+ T -- "Token-Refresh" --> A
84
+ ```
85
+
86
+ **Hauptkomponenten:**
87
+
88
+ | Komponente | Aufgabe |
89
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
90
+ | **ExternalTaskAdapter** | Läuft im Hauptprozess der Next.js App. Überwacht das Dateisystem, startet Worker-Prozesse, verwaltet Tokens und koordiniert Restarts. |
91
+ | **ExternalTaskWorkerProcess** | Eigenständiger Node.js-Kindprozess (einer pro Topic). Lädt den transpilierten Handler, verbindet sich per HTTP-Long-Polling mit der Engine und verarbeitet Tasks. |
92
+ | **ProcessCube Engine** | Verwaltet BPMN-Prozesse und vergibt External Tasks an Worker über das Fetch-and-Lock-Protokoll. |
93
+
94
+ #### Lebenszyklus eines External Tasks
95
+
96
+ Das folgende Sequenzdiagramm zeigt, wie ein External Task von der Engine zum Worker gelangt, verarbeitet und abgeschlossen wird:
97
+
98
+ ```mermaid
99
+ sequenceDiagram
100
+ participant Engine as ProcessCube Engine
101
+ participant Worker as Worker Process
102
+
103
+ loop Polling-Schleife
104
+ Worker->>Engine: POST /external_tasks/fetch_and_lock<br/>(workerId, topic, maxTasks, lockDuration)
105
+ Note right of Engine: Long-Polling — Anfrage wartet<br/>bis Task verfügbar oder Timeout
106
+ Engine-->>Worker: ExternalTask[] (mit Payload)
107
+ end
108
+
109
+ Note over Worker: Handler-Funktion wird aufgerufen:<br/>handler(payload, task, signal)
110
+
111
+ loop Lock-Verlängerung
112
+ Worker->>Engine: PUT /external_tasks/{id}/extend_lock<br/>(additionalDuration)
113
+ Note right of Engine: Lock wird verlängert,<br/>damit der Task nicht abläuft
114
+ end
115
+
116
+ alt Erfolg
117
+ Worker->>Engine: PUT /external_tasks/{id}/finish<br/>(result)
118
+ Note right of Engine: Ergebnis wird als<br/>Prozessvariable gespeichert
119
+ else Fehler
120
+ Worker->>Engine: PUT /external_tasks/{id}/error<br/>(errorCode, errorMessage)
121
+ Note right of Engine: Fehler wird im<br/>Prozess behandelt
122
+ end
123
+ ```
124
+
125
+ **Ablauf im Detail:**
126
+
127
+ 1. Der Worker pollt die Engine per HTTP-Long-Polling nach neuen Tasks für sein Topic.
128
+ 2. Sobald ein Task verfügbar ist, sperrt die Engine ihn (Lock) und liefert ihn mit dem Payload aus.
129
+ 3. Der Worker ruft die Handler-Funktion auf und übergibt Payload, Task-Metadaten und ein AbortSignal.
130
+ 4. Während der Verarbeitung verlängert der Worker automatisch den Lock, damit die Engine den Task nicht vorzeitig freigibt.
131
+ 5. Nach Abschluss meldet der Worker das Ergebnis (Finish) oder einen Fehler (Error) an die Engine.
132
+
133
+ #### Worker-Startup und IPC-Kommunikation
134
+
135
+ Das App SDK startet pro `external_task.ts`-Datei einen eigenen Node.js-Kindprozess. Die Kommunikation zwischen Hauptprozess (Adapter) und Kindprozess (Worker) läuft über IPC (Inter-Process Communication):
136
+
137
+ ```mermaid
138
+ sequenceDiagram
139
+ participant FS as Dateisystem
140
+ participant Adapter as ExternalTaskAdapter<br/>(Hauptprozess)
141
+ participant Worker as Worker Process<br/>(Kindprozess)
142
+ participant Engine as ProcessCube Engine
143
+
144
+ FS->>Adapter: Neue Datei erkannt:<br/>app/order/process/external_task.ts
145
+
146
+ Adapter->>Adapter: Datei transpilieren (esbuild)
147
+ Adapter->>Adapter: Topic ableiten: order/process
148
+
149
+ Adapter->>Worker: fork() — neuer Node.js-Prozess
150
+
151
+ Adapter->>Worker: IPC: { action: "create",<br/>topic, identity, moduleString }
152
+
153
+ Worker->>Worker: Handler-Modul aus String laden
154
+ Worker->>Engine: Polling starten (Fetch & Lock)
155
+ Note over Worker,Engine: Worker verarbeitet Tasks…
156
+
157
+ Note over FS: Datei wird geändert
158
+
159
+ FS->>Adapter: Dateiänderung erkannt
160
+
161
+ Adapter->>Adapter: Datei neu transpilieren
162
+ Adapter->>Worker: IPC: { action: "restart",<br/>topic, identity, moduleString }
163
+
164
+ Worker->>Worker: Alten Worker stoppen
165
+ Worker->>Worker: Neuen Handler laden
166
+ Worker->>Engine: Polling neu starten
167
+
168
+ Note over Adapter: Token läuft ab (85% Lebensdauer)
169
+
170
+ Adapter->>Adapter: Neuen Token holen (OpenID Connect)
171
+ Adapter->>Worker: IPC: { action: "updateIdentity",<br/>identity }
172
+ ```
173
+
174
+ **IPC-Nachrichten:**
175
+
176
+ | Action | Richtung | Beschreibung |
177
+ | ---------------- | ---------------- | ---------------------------------------------------------------------------------- |
178
+ | `create` | Adapter → Worker | Initialer Start: Übergibt Topic, Identity und transpilierten Handler-Code |
179
+ | `restart` | Adapter → Worker | Hot-Reload: Stoppt den alten Worker und startet mit neuem Code (gleiche Worker-ID) |
180
+ | `updateIdentity` | Adapter → Worker | Aktualisiert den Auth-Token auf dem laufenden Worker |
181
+
182
+ #### Fehlerbehandlung und Restart-Strategie
183
+
184
+ Das System hat zwei Ebenen der Fehlerbehandlung: im Worker-Prozess selbst und im Adapter (Hauptprozess).
185
+
186
+ ```mermaid
187
+ flowchart TD
188
+ A[Fehler im Worker] --> B{Verbindungsfehler?<br/>ECONNREFUSED / ETIMEDOUT /<br/>ECONNRESET / etc.}
189
+
190
+ B -- Ja --> C{Retries < Max?<br/>Standard: 6}
191
+ C -- Ja --> D[Exponentieller Backoff<br/>1s → 2s → 4s → … → 30s max]
192
+ D --> E[Erneut verbinden]
193
+ E --> F{Verbindung OK?}
194
+ F -- Ja --> G[Weiter arbeiten ✓]
195
+ F -- Nein --> C
196
+
197
+ C -- Nein --> H[Worker beenden<br/>Exit Code 3]
198
+
199
+ B -- Nein --> I{Uncaught Exception?}
200
+ I -- Ja --> J[Worker beenden<br/>Exit Code 4]
201
+ I -- Nein --> K[Fehler loggen,<br/>Worker beenden<br/>Exit Code 3]
202
+
203
+ H --> L[Adapter erkennt Exit]
204
+ J --> L
205
+ K --> L
206
+
207
+ L --> M{Restarts < 6 in<br/>letzten 5 Minuten?}
208
+ M -- Ja --> N[Exponentieller Backoff<br/>1s → 2s → 4s → … → 30s max]
209
+ N --> O[Worker neu starten]
210
+ O --> P{Start OK?}
211
+ P -- Ja --> G
212
+ P -- Nein --> L
213
+
214
+ M -- Nein --> Q[Worker bleibt gestoppt ✗<br/>Manueller Eingriff nötig]
215
+ ```
216
+
217
+ **Worker-Level (Kindprozess):**
218
+
219
+ - Bei Verbindungsfehlern (ECONNREFUSED, ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN, Socket Hang Up) versucht der Worker bis zu **6 Reconnects** mit exponentiellem Backoff (1s → 2s → 4s → 8s → 16s → 30s max).
220
+ - Die Anzahl der Retries ist über die Umgebungsvariable `PROCESSCUBE_APP_SDK_ETW_RETRY` konfigurierbar.
221
+ - Nach Ausschöpfung der Retries beendet sich der Worker mit Exit Code 3.
222
+ - Bei unbehandelten Exceptions beendet sich der Worker mit Exit Code 4.
223
+
224
+ **Adapter-Level (Hauptprozess):**
225
+
226
+ - Erkennt der Adapter einen Worker-Exit mit Code 3 oder 4, wird ein Neustart versucht.
227
+ - Maximal **6 Neustarts** innerhalb eines **5-Minuten-Fensters** sind erlaubt.
228
+ - Die Backoff-Zeiten steigen exponentiell: 1s → 2s → 4s → 8s → 16s → 30s.
229
+ - Wird das Limit erreicht, bleibt der Worker gestoppt — ein manueller Eingriff (z.B. App-Neustart) ist nötig.
230
+ - Nach Ablauf des 5-Minuten-Fensters wird der Zähler zurückgesetzt.
231
+
232
+ #### Setup und Konfiguration
233
+
234
+ ##### 1. Next.js Plugin aktivieren
235
+
236
+ In der `next.config.js` wird das SDK-Plugin eingebunden und External Tasks aktiviert:
237
+
238
+ ```javascript
239
+ // next.config.js
240
+ const { withApplicationSdk } = require('@5minds/processcube_app_sdk/server');
241
+
242
+ module.exports = withApplicationSdk({
243
+ applicationSdk: {
244
+ useExternalTasks: true,
245
+ // Optional: Eigenes Verzeichnis für External Tasks
246
+ // customExternalTasksDirPath: './my-tasks',
247
+ },
248
+ });
249
+ ```
250
+
251
+ Das Plugin erkennt automatisch, ob die App im Development- oder Production-Modus läuft, und startet die Worker entsprechend. Während des Build-Prozesses (`next build`) werden keine Worker gestartet.
252
+
253
+ ##### 2. Handler-Datei anlegen
254
+
255
+ External Tasks werden durch Dateien mit dem Namen `external_task.ts` (oder `.js`) definiert. Das Verzeichnis, in dem die Datei liegt, bestimmt automatisch das **Topic**, unter dem sich der Worker bei der Engine registriert.
256
+
257
+ ```
258
+ app/
259
+ ├── order/
260
+ │ └── process/
261
+ │ └── external_task.ts → Topic: order/process
262
+ ├── invoice/
263
+ │ └── send/
264
+ │ └── external_task.ts → Topic: invoice/send
265
+ └── notification/
266
+ └── email/
267
+ └── external_task.ts → Topic: notification/email
268
+ ```
269
+
270
+ Das SDK sucht Handler-Dateien standardmäßig in `./app` oder `./src/app`. Ein eigenes Verzeichnis kann über `customExternalTasksDirPath` konfiguriert werden.
271
+
272
+ > **Wichtig:** Pro Verzeichnis darf nur eine `external_task.ts` oder `external_task.js` existieren. Beide Dateien im selben Verzeichnis führen zu einem Fehler.
273
+
274
+ #### Handler-Signatur
275
+
276
+ Der Handler wird als **Default-Export** der Datei definiert. Er erhält bis zu drei Parameter:
277
+
278
+ ```typescript
279
+ export default async function handleExternalTask(payload: any, task: ExternalTask<any>, signal: AbortSignal) {
280
+ // Geschäftslogik hier
281
+ return { result: 'done' };
282
+ }
283
+ ```
284
+
285
+ | Parameter | Typ | Beschreibung |
286
+ | --------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
287
+ | `payload` | `any` | Die Prozessvariablen, die der BPMN-Prozess dem External Task mitgibt. Enthält die im Prozessmodell definierte Payload-Expression. |
288
+ | `task` | `ExternalTask<any>` | Metadaten des Tasks: `id`, `workerId`, `topic`, `correlationId`, `processInstanceId`, `processDefinitionId`, `flowNodeInstanceId`, `lockExpirationTime`, `state`, `createdAt`. Optional. |
289
+ | `signal` | `AbortSignal` | Wird ausgelöst, wenn ein Boundary Event (z.B. Timer) den Task abbricht. Optional. |
290
+
291
+ **Rückgabewert:** Das zurückgegebene Objekt wird als Ergebnis an die Engine gemeldet und steht im BPMN-Prozess als Variable zur Verfügung.
292
+
293
+ #### Worker-Konfiguration
294
+
295
+ Über einen benannten `config`-Export können Worker-Einstellungen pro Handler angepasst werden:
296
+
297
+ ```typescript
298
+ import { ExternalTaskConfig } from '@5minds/processcube_app_sdk/server';
299
+
300
+ export const config: ExternalTaskConfig = {
301
+ lockDuration: 5000, // Lock-Dauer in ms (Standard: 30000)
302
+ maxTasks: 5, // Gleichzeitige Tasks pro Polling-Zyklus (Standard: 10)
303
+ };
304
+ ```
305
+
306
+ | Option | Typ | Standard | Beschreibung |
307
+ | -------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
308
+ | `lockDuration` | `number` | `30000` | Dauer in Millisekunden, für die ein Task gesperrt wird. Bestimmt auch das Intervall der Lock-Verlängerung und die maximale Verzögerung bei Abort-Signalen. |
309
+ | `maxTasks` | `number` | `10` | Maximale Anzahl gleichzeitig abgeholter Tasks pro Polling-Zyklus. |
310
+
311
+ #### Abort-Handling bei Boundary Events
312
+
313
+ Wenn ein BPMN Boundary Event (z.B. ein Timer oder Signal) einen External Task abbricht, löst die Engine den Abbruch beim nächsten Lock-Renewal aus. Das `AbortSignal` im Handler wird daraufhin ausgelöst.
314
+
315
+ **Wichtig:** Die `lockDuration` bestimmt die maximale Verzögerung bis zum Abort, da die Engine den Abbruch erst beim nächsten Lock-Renewal mitteilen kann:
316
+
317
+ | lockDuration | Max. Verzögerung bis Abort |
318
+ | ------------------ | -------------------------- |
319
+ | `30000` (Standard) | bis zu 30 Sekunden |
320
+ | `5000` | bis zu 5 Sekunden |
321
+ | `1000` | bis zu 1 Sekunde |
322
+
323
+ Für zeitkritische Abbrüche sollte die `lockDuration` entsprechend reduziert werden.
324
+
325
+ **Beispiel mit Abort-Handling:**
326
+
327
+ ```typescript
328
+ import { ExternalTaskConfig } from '@5minds/processcube_app_sdk/server';
329
+
330
+ export const config: ExternalTaskConfig = {
331
+ lockDuration: 5000,
332
+ };
333
+
334
+ export default async function handleExternalTask(payload: any, _task: any, signal: AbortSignal) {
335
+ // Listener für Cleanup-Aktionen bei Abbruch
336
+ signal.addEventListener(
337
+ 'abort',
338
+ () => {
339
+ console.log('Task wurde durch Boundary Event abgebrochen');
340
+ // Hier ggf. Ressourcen freigeben
341
+ },
342
+ { once: true },
343
+ );
344
+
345
+ // Signal vor asynchronen Operationen prüfen
346
+ if (signal.aborted) return;
347
+
348
+ const result = await doWork(payload);
349
+
350
+ // Signal nach asynchronen Operationen prüfen
351
+ if (signal.aborted) return;
352
+
353
+ return result;
354
+ }
355
+ ```
356
+
357
+ #### Authentifizierung und Token-Management
358
+
359
+ Ist eine ProcessCube Authority konfiguriert, holt der Adapter automatisch Tokens per **OpenID Connect Client Credentials Grant** und verteilt sie an alle Worker.
360
+
361
+ ```mermaid
362
+ sequenceDiagram
363
+ participant Authority as OpenID Authority
364
+ participant Adapter as ExternalTaskAdapter
365
+ participant W1 as Worker 1
366
+ participant W2 as Worker 2
367
+
368
+ Adapter->>Authority: Client Credentials Grant<br/>(client_id, client_secret, scope: engine_etw)
369
+ Authority-->>Adapter: TokenSet (access_token, expires_in)
370
+
371
+ Adapter->>W1: IPC: create (mit Identity)
372
+ Adapter->>W2: IPC: create (mit Identity)
373
+
374
+ Note over Adapter: Token-Refresh bei 85%<br/>der Lebensdauer
375
+
376
+ Adapter->>Authority: Client Credentials Grant (Refresh)
377
+ Authority-->>Adapter: Neues TokenSet
378
+
379
+ Adapter->>W1: IPC: updateIdentity
380
+ Adapter->>W2: IPC: updateIdentity
381
+ ```
382
+
383
+ - Der Token wird bei **85% seiner Lebensdauer** automatisch erneuert.
384
+ - Alle aktiven Worker erhalten den neuen Token per IPC-Nachricht.
385
+ - Der initiale Token-Abruf hat **10 Versuche** mit exponentiellem Backoff (max. 30s).
386
+ - Der periodische Token-Refresh versucht es **unbegrenzt** mit Backoff (max. 60s).
387
+ - Ist keine Authority konfiguriert, wird eine Dummy-Identity verwendet (für lokale Entwicklung ohne Auth).
388
+
389
+ #### Umgebungsvariablen
390
+
391
+ | Variable | Pflicht | Standard | Beschreibung |
392
+ | ------------------------------------------------ | -------------- | ------------------------ | ---------------------------------------------------------------------------------------- |
393
+ | `PROCESSCUBE_ENGINE_URL` | Nein | `http://localhost:10560` | URL der ProcessCube Engine |
394
+ | `PROCESSCUBE_AUTHORITY_URL` | Nein | — | URL des OpenID-Providers. Wenn gesetzt, wird Token-basierte Authentifizierung aktiviert. |
395
+ | `PROCESSCUBE_EXTERNAL_TASK_WORKER_CLIENT_ID` | Wenn Authority | — | Client-ID für den OpenID Client Credentials Grant |
396
+ | `PROCESSCUBE_EXTERNAL_TASK_WORKER_CLIENT_SECRET` | Wenn Authority | — | Client-Secret für den OpenID Client Credentials Grant |
397
+ | `PROCESSCUBE_APP_SDK_ETW_RETRY` | Nein | `6` | Maximale Anzahl der Reconnect-Versuche im Worker-Prozess bei Verbindungsfehlern |
398
+
399
+ #### Vollständiges Beispiel
400
+
401
+ Eine `external_task.ts` mit allen Features — Konfiguration, typisiertem Payload, Fehlerbehandlung und Abort-Support:
402
+
403
+ ```typescript
404
+ import { ExternalTaskConfig } from '@5minds/processcube_app_sdk/server';
405
+
406
+ // Worker-Konfiguration
407
+ export const config: ExternalTaskConfig = {
408
+ lockDuration: 5000, // 5s Lock für schnelle Abort-Reaktion
409
+ maxTasks: 3, // Maximal 3 Tasks gleichzeitig
410
+ };
411
+
412
+ // Typen für Payload und Ergebnis
413
+ interface OrderPayload {
414
+ orderId: string;
415
+ customerEmail: string;
416
+ items: Array<{ productId: string; quantity: number }>;
417
+ }
418
+
419
+ interface OrderResult {
420
+ confirmationId: string;
421
+ processedAt: string;
422
+ }
423
+
424
+ export default async function handleExternalTask(payload: OrderPayload, task: any, signal: AbortSignal): Promise<OrderResult | undefined> {
425
+ console.log(`Verarbeite Bestellung ${payload.orderId} (Task: ${task.id})`);
426
+
427
+ // Abort-Handler für Cleanup
428
+ signal.addEventListener(
429
+ 'abort',
430
+ () => {
431
+ console.log(`Bestellung ${payload.orderId} wurde abgebrochen`);
432
+ },
433
+ { once: true },
434
+ );
435
+
436
+ if (signal.aborted) return;
437
+
438
+ // Geschäftslogik
439
+ const confirmation = await processOrder(payload);
440
+
441
+ if (signal.aborted) return;
442
+
443
+ await sendConfirmationEmail(payload.customerEmail, confirmation);
444
+
445
+ if (signal.aborted) return;
446
+
447
+ return {
448
+ confirmationId: confirmation.id,
449
+ processedAt: new Date().toISOString(),
450
+ };
451
+ }
452
+ ```
453
+
454
+ #### Hot-Reload
455
+
456
+ Im Development-Modus überwacht das SDK die Handler-Dateien per File-Watcher. Änderungen an einer `external_task.ts` werden automatisch erkannt: Die Datei wird neu transpiliert und der Worker per IPC-Nachricht (`restart`) mit dem neuen Code neu gestartet — ohne die App neu starten zu müssen. Die Worker-ID bleibt dabei erhalten.
457
+
49
458
  ## Wie kann ich das Projekt aufsetzen?
50
459
 
51
460
  ### Setup/Installation