@gravito/flux 3.0.2 → 4.0.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.
@@ -1,117 +1,17 @@
1
1
  import {
2
- BunSQLiteStorage
3
- } from "./chunk-NAIVO7RR.js";
4
-
5
- // src/errors.ts
6
- var FluxErrorCode = /* @__PURE__ */ ((FluxErrorCode2) => {
7
- FluxErrorCode2["WORKFLOW_NOT_FOUND"] = "WORKFLOW_NOT_FOUND";
8
- FluxErrorCode2["WORKFLOW_INVALID_INPUT"] = "WORKFLOW_INVALID_INPUT";
9
- FluxErrorCode2["WORKFLOW_DEFINITION_CHANGED"] = "WORKFLOW_DEFINITION_CHANGED";
10
- FluxErrorCode2["WORKFLOW_NAME_MISMATCH"] = "WORKFLOW_NAME_MISMATCH";
11
- FluxErrorCode2["INVALID_STATE_TRANSITION"] = "INVALID_STATE_TRANSITION";
12
- FluxErrorCode2["WORKFLOW_NOT_SUSPENDED"] = "WORKFLOW_NOT_SUSPENDED";
13
- FluxErrorCode2["INVALID_STEP_INDEX"] = "INVALID_STEP_INDEX";
14
- FluxErrorCode2["STEP_TIMEOUT"] = "STEP_TIMEOUT";
15
- FluxErrorCode2["STEP_NOT_FOUND"] = "STEP_NOT_FOUND";
16
- FluxErrorCode2["CONCURRENT_MODIFICATION"] = "CONCURRENT_MODIFICATION";
17
- FluxErrorCode2["EMPTY_WORKFLOW"] = "EMPTY_WORKFLOW";
18
- FluxErrorCode2["NO_RECOVERY_ACTION"] = "NO_RECOVERY_ACTION";
19
- FluxErrorCode2["INVALID_JSON_POINTER"] = "INVALID_JSON_POINTER";
20
- FluxErrorCode2["INVALID_PATH_TRAVERSAL"] = "INVALID_PATH_TRAVERSAL";
21
- FluxErrorCode2["CANNOT_REPLACE_ROOT"] = "CANNOT_REPLACE_ROOT";
22
- FluxErrorCode2["CANNOT_REMOVE_ROOT"] = "CANNOT_REMOVE_ROOT";
23
- return FluxErrorCode2;
24
- })(FluxErrorCode || {});
25
- var FluxError = class extends Error {
26
- /**
27
- * Creates a new FluxError.
28
- *
29
- * @param message - Human-readable error description.
30
- * @param code - Machine-readable error code.
31
- * @param context - Additional metadata related to the error.
32
- */
33
- constructor(message, code, context) {
34
- super(message);
35
- this.code = code;
36
- this.context = context;
37
- this.name = "FluxError";
38
- }
39
- };
40
- function workflowNotFound(id) {
41
- return new FluxError(`Workflow not found: ${id}`, "WORKFLOW_NOT_FOUND" /* WORKFLOW_NOT_FOUND */, {
42
- workflowId: id
43
- });
44
- }
45
- function invalidStateTransition(from, to) {
46
- return new FluxError(
47
- `Invalid state transition: ${from} \u2192 ${to}`,
48
- "INVALID_STATE_TRANSITION" /* INVALID_STATE_TRANSITION */,
49
- { from, to }
50
- );
51
- }
52
- function invalidInput(workflowName) {
53
- return new FluxError(
54
- `Invalid input for workflow "${workflowName}"`,
55
- "WORKFLOW_INVALID_INPUT" /* WORKFLOW_INVALID_INPUT */,
56
- { workflowName }
57
- );
58
- }
59
- function workflowNameMismatch(expected, received) {
60
- return new FluxError(
61
- `Workflow name mismatch: ${received} !== ${expected}`,
62
- "WORKFLOW_NAME_MISMATCH" /* WORKFLOW_NAME_MISMATCH */,
63
- { expected, received }
64
- );
65
- }
66
- function workflowDefinitionChanged() {
67
- return new FluxError(
68
- "Workflow definition changed; operation is not safe",
69
- "WORKFLOW_DEFINITION_CHANGED" /* WORKFLOW_DEFINITION_CHANGED */
70
- );
71
- }
72
- function workflowNotSuspended(status) {
73
- return new FluxError(
74
- `Workflow is not suspended (status: ${status})`,
75
- "WORKFLOW_NOT_SUSPENDED" /* WORKFLOW_NOT_SUSPENDED */,
76
- { status }
77
- );
78
- }
79
- function stepNotFound(step) {
80
- return new FluxError(`Step not found: ${step}`, "STEP_NOT_FOUND" /* STEP_NOT_FOUND */, { step });
81
- }
82
- function invalidStepIndex(index) {
83
- return new FluxError(`Invalid step index: ${index}`, "INVALID_STEP_INDEX" /* INVALID_STEP_INDEX */, { index });
84
- }
85
- function emptyWorkflow(workflowName) {
86
- return new FluxError(`Workflow "${workflowName}" has no steps`, "EMPTY_WORKFLOW" /* EMPTY_WORKFLOW */, {
87
- workflowName
88
- });
89
- }
90
- function noRecoveryAction(stepName) {
91
- return new FluxError(
92
- `No recovery action registered for step: ${stepName}`,
93
- "NO_RECOVERY_ACTION" /* NO_RECOVERY_ACTION */,
94
- { stepName }
95
- );
96
- }
97
- function invalidJsonPointer(path) {
98
- return new FluxError(`Invalid JSON Pointer: ${path}`, "INVALID_JSON_POINTER" /* INVALID_JSON_POINTER */, {
99
- path
100
- });
101
- }
102
- function invalidPathTraversal(segment, current) {
103
- return new FluxError(
104
- `Cannot access property '${segment}' on ${current}`,
105
- "INVALID_PATH_TRAVERSAL" /* INVALID_PATH_TRAVERSAL */,
106
- { segment, currentType: typeof current }
107
- );
108
- }
109
- function cannotReplaceRoot() {
110
- return new FluxError("Cannot replace root object", "CANNOT_REPLACE_ROOT" /* CANNOT_REPLACE_ROOT */);
111
- }
112
- function cannotRemoveRoot() {
113
- return new FluxError("Cannot remove root object", "CANNOT_REMOVE_ROOT" /* CANNOT_REMOVE_ROOT */);
114
- }
2
+ BunSQLiteStorage,
3
+ FluxError,
4
+ emptyWorkflow,
5
+ invalidInput,
6
+ invalidStateTransition,
7
+ invalidStepIndex,
8
+ noRecoveryAction,
9
+ stepNotFound,
10
+ workflowDefinitionChanged,
11
+ workflowNameMismatch,
12
+ workflowNotFound,
13
+ workflowNotSuspended
14
+ } from "./chunk-UZKSACBE.js";
115
15
 
116
16
  // src/builder/WorkflowBuilder.ts
117
17
  var WorkflowBuilder = class {
@@ -1544,7 +1444,11 @@ function resetHistoryFrom(ctx, startIndex) {
1544
1444
  entry.status = "pending";
1545
1445
  entry.startedAt = void 0;
1546
1446
  entry.completedAt = void 0;
1447
+ entry.suspendedAt = void 0;
1448
+ entry.compensatedAt = void 0;
1449
+ entry.waitingFor = void 0;
1547
1450
  entry.duration = void 0;
1451
+ entry.output = void 0;
1548
1452
  entry.error = void 0;
1549
1453
  entry.retries = 0;
1550
1454
  }
@@ -1954,6 +1858,7 @@ var RollbackManager = class {
1954
1858
  error: originalError.message
1955
1859
  });
1956
1860
  let compensatedCount = 0;
1861
+ let skippedCount = 0;
1957
1862
  for (let i = failedAtIndex - 1; i >= 0; i--) {
1958
1863
  const step = definition.steps[i];
1959
1864
  let execution = currentCtx.history[i];
@@ -1988,11 +1893,36 @@ var RollbackManager = class {
1988
1893
  } catch (err) {
1989
1894
  const error = err instanceof Error ? err : new Error(String(err));
1990
1895
  const action = this.recoveryManager.getAction(step.name);
1896
+ if (action?.type === "retry") {
1897
+ const retryResult = await new CompensationRetryPolicy({
1898
+ ...this.retryPolicy.getConfig(),
1899
+ maxAttempts: action.maxAttempts ?? this.retryPolicy.getConfig().maxAttempts
1900
+ }).execute(async () => {
1901
+ await step.compensate?.(currentCtx);
1902
+ });
1903
+ if (retryResult.success) {
1904
+ execution = { ...execution, status: "compensated", compensatedAt: /* @__PURE__ */ new Date() };
1905
+ currentCtx = updateWorkflowContext(currentCtx, {
1906
+ history: currentCtx.history.map((h, idx) => idx === i ? execution : h)
1907
+ });
1908
+ compensatedCount++;
1909
+ await this.traceEmitter.stepCompensate(currentCtx, step.name, i);
1910
+ currentCtx = await this.persist(currentCtx);
1911
+ continue;
1912
+ }
1913
+ }
1991
1914
  if (action?.type === "skip") {
1915
+ execution = { ...execution, status: "completed", error: error.message };
1916
+ currentCtx = updateWorkflowContext(currentCtx, {
1917
+ history: currentCtx.history.map((h, idx) => idx === i ? execution : h)
1918
+ });
1919
+ currentCtx = await this.persist(currentCtx);
1920
+ skippedCount++;
1992
1921
  continue;
1993
1922
  }
1994
1923
  if (action?.type === "abort") {
1995
1924
  currentCtx = updateWorkflowContext(currentCtx, { status: "compensation_failed" });
1925
+ currentCtx = await this.persist(currentCtx);
1996
1926
  await this.traceEmitter.emit({
1997
1927
  type: "workflow:error",
1998
1928
  timestamp: Date.now(),
@@ -2005,6 +1935,7 @@ var RollbackManager = class {
2005
1935
  }
2006
1936
  await this.recoveryManager.notifyRecoveryNeeded(currentCtx, step.name, error);
2007
1937
  currentCtx = updateWorkflowContext(currentCtx, { status: "compensation_failed" });
1938
+ currentCtx = await this.persist(currentCtx);
2008
1939
  await this.traceEmitter.emit({
2009
1940
  type: "workflow:error",
2010
1941
  timestamp: Date.now(),
@@ -2017,7 +1948,7 @@ var RollbackManager = class {
2017
1948
  }
2018
1949
  currentCtx = await this.persist(currentCtx);
2019
1950
  }
2020
- if (compensatedCount > 0) {
1951
+ if (compensatedCount > 0 || skippedCount > 0) {
2021
1952
  currentCtx = updateWorkflowContext(currentCtx, { status: "rolled_back" });
2022
1953
  await this.traceEmitter.emit({
2023
1954
  type: "workflow:rollback_complete",
@@ -2712,7 +2643,14 @@ var FluxEngine = class {
2712
2643
  }
2713
2644
  ctx = updateWorkflowContext(ctx, {
2714
2645
  history: ctx.history.map(
2715
- (h, i) => i === idx ? { ...h, status: "completed", completedAt: /* @__PURE__ */ new Date(), output: payload } : h
2646
+ (h, i) => i === idx ? {
2647
+ ...h,
2648
+ status: "completed",
2649
+ completedAt: /* @__PURE__ */ new Date(),
2650
+ suspendedAt: void 0,
2651
+ waitingFor: void 0,
2652
+ output: payload
2653
+ } : h
2716
2654
  )
2717
2655
  });
2718
2656
  await this.traceEmitter.emit({
@@ -2831,7 +2769,7 @@ var PostgreSQLStorage = class {
2831
2769
  */
2832
2770
  constructor(options = {}) {
2833
2771
  this.options = options;
2834
- this.tableName = options.tableName ?? "flux_workflows";
2772
+ this.tableName = validateSqlIdentifier(options.tableName ?? "flux_workflows", "tableName");
2835
2773
  }
2836
2774
  /**
2837
2775
  * Initializes the database connection pool and schema.
@@ -3056,20 +2994,36 @@ var PostgreSQLStorage = class {
3056
2994
  await this.pool.query("VACUUM ANALYZE");
3057
2995
  }
3058
2996
  };
2997
+ function validateSqlIdentifier(value, field) {
2998
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
2999
+ throw new FluxError(
3000
+ `Invalid ${field}: "${value}". Only letters, numbers, and underscores are allowed.`,
3001
+ "WORKFLOW_INVALID_INPUT" /* WORKFLOW_INVALID_INPUT */,
3002
+ { field, value }
3003
+ );
3004
+ }
3005
+ return value;
3006
+ }
3059
3007
 
3060
3008
  // src/trace/JsonFileTraceSink.ts
3061
- import { appendFile, mkdir, writeFile } from "fs/promises";
3009
+ import { mkdir } from "fs/promises";
3062
3010
  import { dirname } from "path";
3011
+ import { getDefaultRuntimeAdapter } from "@gravito/core";
3063
3012
  var JsonFileTraceSink = class {
3064
3013
  path;
3065
3014
  ready;
3015
+ fileSink = null;
3016
+ buffer = [];
3017
+ bufferSize;
3018
+ adapter = getDefaultRuntimeAdapter();
3066
3019
  /**
3067
- * Creates a new JSON file trace sink.
3020
+ * Creates a new JSON file trace sink with buffering support.
3068
3021
  *
3069
3022
  * @param options - Configuration options for the sink.
3070
3023
  */
3071
3024
  constructor(options) {
3072
3025
  this.path = options.path;
3026
+ this.bufferSize = options.bufferSize ?? 50;
3073
3027
  this.ready = this.init(options.reset ?? true);
3074
3028
  }
3075
3029
  /**
@@ -3080,14 +3034,22 @@ var JsonFileTraceSink = class {
3080
3034
  */
3081
3035
  async init(reset) {
3082
3036
  await mkdir(dirname(this.path), { recursive: true });
3083
- if (reset) {
3084
- await writeFile(this.path, "", "utf8");
3037
+ if (this.adapter.createFileSink) {
3038
+ this.fileSink = await this.adapter.createFileSink?.(this.path);
3039
+ }
3040
+ if (reset && !this.fileSink) {
3041
+ if (this.adapter.writeFile) {
3042
+ await this.adapter.writeFile(this.path, "");
3043
+ }
3044
+ } else if (reset && this.fileSink) {
3045
+ this.fileSink.write("");
3085
3046
  }
3086
3047
  }
3087
3048
  /**
3088
3049
  * Appends a trace event to the file in NDJSON format.
3089
3050
  *
3090
- * Waits for initialization to complete before writing.
3051
+ * Events are buffered and written in batches using FileSink
3052
+ * for improved I/O efficiency. Waits for initialization to complete before writing.
3091
3053
  *
3092
3054
  * @param event - The trace event to record.
3093
3055
  * @throws {Error} If writing to the file fails.
@@ -3104,8 +3066,48 @@ var JsonFileTraceSink = class {
3104
3066
  */
3105
3067
  async emit(event) {
3106
3068
  await this.ready;
3107
- await appendFile(this.path, `${JSON.stringify(event)}
3108
- `, "utf8");
3069
+ const eventLine = `${JSON.stringify(event)}
3070
+ `;
3071
+ if (this.fileSink) {
3072
+ this.buffer.push(eventLine);
3073
+ if (this.buffer.length >= this.bufferSize) {
3074
+ await this.flushBuffer();
3075
+ }
3076
+ } else {
3077
+ if (this.adapter.appendFile) {
3078
+ await this.adapter.appendFile(this.path, eventLine);
3079
+ }
3080
+ }
3081
+ }
3082
+ /**
3083
+ * Flushes buffered events to disk.
3084
+ *
3085
+ * This is called automatically when the buffer reaches the configured size.
3086
+ * Can also be called manually to ensure all events are written.
3087
+ *
3088
+ * @throws {Error} If flushing fails.
3089
+ */
3090
+ async flushBuffer() {
3091
+ if (!this.fileSink || this.buffer.length === 0) {
3092
+ return;
3093
+ }
3094
+ const content = this.buffer.join("");
3095
+ this.fileSink.write(content);
3096
+ this.buffer = [];
3097
+ await this.fileSink.flush();
3098
+ }
3099
+ /**
3100
+ * Closes the sink and flushes any remaining buffered events.
3101
+ *
3102
+ * Should be called before process exit to ensure all events are persisted.
3103
+ *
3104
+ * @throws {Error} If closing or flushing fails.
3105
+ */
3106
+ async close() {
3107
+ await this.flushBuffer();
3108
+ if (this.fileSink) {
3109
+ await this.fileSink.end();
3110
+ }
3109
3111
  }
3110
3112
  };
3111
3113
 
@@ -3423,6 +3425,16 @@ var OrbitFlux = class _OrbitFlux {
3423
3425
  };
3424
3426
  this.engine = new FluxEngine(engineConfig);
3425
3427
  core.container.instance(exposeAs, this.engine);
3428
+ const health = core.container.make("health");
3429
+ if (health) {
3430
+ const engine = this.engine;
3431
+ health.register("flux", async () => ({
3432
+ status: engine ? "healthy" : "unhealthy",
3433
+ details: {
3434
+ storage: typeof storage === "string" ? storage : "custom"
3435
+ }
3436
+ }));
3437
+ }
3426
3438
  core.logger.info(
3427
3439
  `[OrbitFlux] Initialized (Storage: ${typeof storage === "string" ? storage : "custom"})`
3428
3440
  );
@@ -3450,22 +3462,6 @@ var OrbitFlux = class _OrbitFlux {
3450
3462
  };
3451
3463
 
3452
3464
  export {
3453
- FluxErrorCode,
3454
- FluxError,
3455
- workflowNotFound,
3456
- invalidStateTransition,
3457
- invalidInput,
3458
- workflowNameMismatch,
3459
- workflowDefinitionChanged,
3460
- workflowNotSuspended,
3461
- stepNotFound,
3462
- invalidStepIndex,
3463
- emptyWorkflow,
3464
- noRecoveryAction,
3465
- invalidJsonPointer,
3466
- invalidPathTraversal,
3467
- cannotReplaceRoot,
3468
- cannotRemoveRoot,
3469
3465
  WorkflowBuilder,
3470
3466
  createWorkflow,
3471
3467
  BatchExecutor,
@@ -3483,4 +3479,4 @@ export {
3483
3479
  FluxSilentLogger,
3484
3480
  OrbitFlux
3485
3481
  };
3486
- //# sourceMappingURL=chunk-4DXCQ6CL.js.map
3482
+ //# sourceMappingURL=chunk-JUCSCBHA.js.map