@effect/workflow 0.1.1 → 0.1.3

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 (38) hide show
  1. package/README.md +13 -1
  2. package/WorkflowProxy/package.json +6 -0
  3. package/WorkflowProxyServer/package.json +6 -0
  4. package/dist/cjs/Activity.js +6 -11
  5. package/dist/cjs/Activity.js.map +1 -1
  6. package/dist/cjs/Workflow.js +37 -6
  7. package/dist/cjs/Workflow.js.map +1 -1
  8. package/dist/cjs/WorkflowProxy.js +111 -0
  9. package/dist/cjs/WorkflowProxy.js.map +1 -0
  10. package/dist/cjs/WorkflowProxyServer.js +61 -0
  11. package/dist/cjs/WorkflowProxyServer.js.map +1 -0
  12. package/dist/cjs/index.js +5 -1
  13. package/dist/dts/Activity.d.ts +1 -17
  14. package/dist/dts/Activity.d.ts.map +1 -1
  15. package/dist/dts/Workflow.d.ts +85 -1
  16. package/dist/dts/Workflow.d.ts.map +1 -1
  17. package/dist/dts/WorkflowProxy.d.ts +90 -0
  18. package/dist/dts/WorkflowProxy.d.ts.map +1 -0
  19. package/dist/dts/WorkflowProxyServer.d.ts +27 -0
  20. package/dist/dts/WorkflowProxyServer.d.ts.map +1 -0
  21. package/dist/dts/index.d.ts +8 -0
  22. package/dist/dts/index.d.ts.map +1 -1
  23. package/dist/esm/Activity.js +4 -10
  24. package/dist/esm/Activity.js.map +1 -1
  25. package/dist/esm/Workflow.js +36 -4
  26. package/dist/esm/Workflow.js.map +1 -1
  27. package/dist/esm/WorkflowProxy.js +101 -0
  28. package/dist/esm/WorkflowProxy.js.map +1 -0
  29. package/dist/esm/WorkflowProxyServer.js +52 -0
  30. package/dist/esm/WorkflowProxyServer.js.map +1 -0
  31. package/dist/esm/index.js +8 -0
  32. package/dist/esm/index.js.map +1 -1
  33. package/package.json +20 -2
  34. package/src/Activity.ts +6 -34
  35. package/src/Workflow.ts +155 -2
  36. package/src/WorkflowProxy.ts +178 -0
  37. package/src/WorkflowProxyServer.ts +103 -0
  38. package/src/index.ts +10 -0
@@ -0,0 +1,52 @@
1
+ import * as HttpApiBuilder from "@effect/platform/HttpApiBuilder";
2
+ import * as Context from "effect/Context";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ /**
6
+ * @since 1.0.0
7
+ * @category Layers
8
+ */
9
+ export const layerHttpApi = (api, name, workflows) => HttpApiBuilder.group(api, name, Effect.fnUntraced(function* (handlers_) {
10
+ let handlers = handlers_;
11
+ for (const workflow_ of workflows) {
12
+ const workflow = workflow_;
13
+ handlers = handlers.handle(workflow.name, ({
14
+ payload
15
+ }) => workflow.execute(payload)).handle(workflow.name + "Discard", ({
16
+ payload
17
+ }) => workflow.execute(payload, {
18
+ discard: true
19
+ }));
20
+ }
21
+ return handlers;
22
+ }));
23
+ /**
24
+ * @since 1.0.0
25
+ * @category Layers
26
+ */
27
+ export const layerRpcHandlers = (workflows, options) => Layer.effectContext(Effect.gen(function* () {
28
+ const context = yield* Effect.context();
29
+ const prefix = options?.prefix ?? "";
30
+ const handlers = new Map();
31
+ for (const workflow_ of workflows) {
32
+ const workflow = workflow_;
33
+ const tag = `${prefix}${workflow.name}`;
34
+ const tagDiscard = `${tag}Discard`;
35
+ const key = `@effect/rpc/Rpc/${tag}`;
36
+ const keyDiscard = `${key}Discard`;
37
+ handlers.set(key, {
38
+ context,
39
+ tag,
40
+ handler: payload => workflow.execute(payload)
41
+ });
42
+ handlers.set(keyDiscard, {
43
+ context,
44
+ tag: tagDiscard,
45
+ handler: payload => workflow.execute(payload, {
46
+ discard: true
47
+ })
48
+ });
49
+ }
50
+ return Context.unsafeMake(handlers);
51
+ }));
52
+ //# sourceMappingURL=WorkflowProxyServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkflowProxyServer.js","names":["HttpApiBuilder","Context","Effect","Layer","layerHttpApi","api","name","workflows","group","fnUntraced","handlers_","handlers","workflow_","workflow","handle","payload","execute","discard","layerRpcHandlers","options","effectContext","gen","context","prefix","Map","tag","tagDiscard","key","keyDiscard","set","handler","unsafeMake"],"sources":["../../src/WorkflowProxyServer.ts"],"sourcesContent":[null],"mappings":"AAIA,OAAO,KAAKA,cAAc,MAAM,iCAAiC;AAIjE,OAAO,KAAKC,OAAO,MAAM,gBAAgB;AACzC,OAAO,KAAKC,MAAM,MAAM,eAAe;AACvC,OAAO,KAAKC,KAAK,MAAM,cAAc;AAIrC;;;;AAIA,OAAO,MAAMC,YAAY,GAAGA,CAQ1BC,GAA+C,EAC/CC,IAAU,EACVC,SAAoB,KAMpBP,cAAc,CAACQ,KAAK,CAClBH,GAAG,EACHC,IAAI,EACJJ,MAAM,CAACO,UAAU,CAAC,WAAUC,SAAS;EACnC,IAAIC,QAAQ,GAAGD,SAAgB;EAC/B,KAAK,MAAME,SAAS,IAAIL,SAAS,EAAE;IACjC,MAAMM,QAAQ,GAAGD,SAAqD;IACtED,QAAQ,GAAGA,QAAQ,CAChBG,MAAM,CACLD,QAAQ,CAACP,IAAW,EACpB,CAAC;MAAES;IAAO,CAAoB,KAAKF,QAAQ,CAACG,OAAO,CAACD,OAAO,CAAC,CAC7D,CACAD,MAAM,CACLD,QAAQ,CAACP,IAAI,GAAG,SAAgB,EAChC,CAAC;MAAES;IAAO,CAAoB,KAAKF,QAAQ,CAACG,OAAO,CAACD,OAAO,EAAE;MAAEE,OAAO,EAAE;IAAI,CAAS,CAAC,CACvF;EACL;EACA,OAAON,QAAwD;AACjE,CAAC,CAAC,CACH;AAEH;;;;AAIA,OAAO,MAAMO,gBAAgB,GAAGA,CAG9BX,SAAoB,EAAEY,OAEvB,KAKChB,KAAK,CAACiB,aAAa,CAAClB,MAAM,CAACmB,GAAG,CAAC,aAAS;EACtC,MAAMC,OAAO,GAAG,OAAOpB,MAAM,CAACoB,OAAO,EAAS;EAC9C,MAAMC,MAAM,GAAGJ,OAAO,EAAEI,MAAM,IAAI,EAAE;EACpC,MAAMZ,QAAQ,GAAG,IAAIa,GAAG,EAA+B;EACvD,KAAK,MAAMZ,SAAS,IAAIL,SAAS,EAAE;IACjC,MAAMM,QAAQ,GAAGD,SAAqD;IACtE,MAAMa,GAAG,GAAG,GAAGF,MAAM,GAAGV,QAAQ,CAACP,IAAI,EAAE;IACvC,MAAMoB,UAAU,GAAG,GAAGD,GAAG,SAAS;IAClC,MAAME,GAAG,GAAG,mBAAmBF,GAAG,EAAE;IACpC,MAAMG,UAAU,GAAG,GAAGD,GAAG,SAAS;IAClChB,QAAQ,CAACkB,GAAG,CAACF,GAAG,EAAE;MAChBL,OAAO;MACPG,GAAG;MACHK,OAAO,EAAGf,OAAY,IAAKF,QAAQ,CAACG,OAAO,CAACD,OAAO;KAC7C,CAAC;IACTJ,QAAQ,CAACkB,GAAG,CAACD,UAAU,EAAE;MACvBN,OAAO;MACPG,GAAG,EAAEC,UAAU;MACfI,OAAO,EAAGf,OAAY,IAAKF,QAAQ,CAACG,OAAO,CAACD,OAAO,EAAE;QAAEE,OAAO,EAAE;MAAI,CAAS;KACvE,CAAC;EACX;EACA,OAAOhB,OAAO,CAAC8B,UAAU,CAACpB,QAAQ,CAAC;AACrC,CAAC,CAAC,CAAC","ignoreList":[]}
package/dist/esm/index.js CHANGED
@@ -18,4 +18,12 @@ export * as Workflow from "./Workflow.js";
18
18
  * @since 1.0.0
19
19
  */
20
20
  export * as WorkflowEngine from "./WorkflowEngine.js";
21
+ /**
22
+ * @since 1.0.0
23
+ */
24
+ export * as WorkflowProxy from "./WorkflowProxy.js";
25
+ /**
26
+ * @since 1.0.0
27
+ */
28
+ export * as WorkflowProxyServer from "./WorkflowProxyServer.js";
21
29
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["Activity","DurableClock","DurableDeferred","Workflow","WorkflowEngine"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;AAGA,OAAO,KAAKA,QAAQ,MAAM,eAAe;AAEzC;;;AAGA,OAAO,KAAKC,YAAY,MAAM,mBAAmB;AAEjD;;;AAGA,OAAO,KAAKC,eAAe,MAAM,sBAAsB;AAEvD;;;AAGA,OAAO,KAAKC,QAAQ,MAAM,eAAe;AAEzC;;;AAGA,OAAO,KAAKC,cAAc,MAAM,qBAAqB","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["Activity","DurableClock","DurableDeferred","Workflow","WorkflowEngine","WorkflowProxy","WorkflowProxyServer"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;AAGA,OAAO,KAAKA,QAAQ,MAAM,eAAe;AAEzC;;;AAGA,OAAO,KAAKC,YAAY,MAAM,mBAAmB;AAEjD;;;AAGA,OAAO,KAAKC,eAAe,MAAM,sBAAsB;AAEvD;;;AAGA,OAAO,KAAKC,QAAQ,MAAM,eAAe;AAEzC;;;AAGA,OAAO,KAAKC,cAAc,MAAM,qBAAqB;AAErD;;;AAGA,OAAO,KAAKC,aAAa,MAAM,oBAAoB;AAEnD;;;AAGA,OAAO,KAAKC,mBAAmB,MAAM,0BAA0B","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect/workflow",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Durable workflows for Effect",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,7 +11,9 @@
11
11
  "sideEffects": [],
12
12
  "homepage": "https://effect.website",
13
13
  "peerDependencies": {
14
- "effect": "^3.16.3"
14
+ "@effect/platform": "^0.84.6",
15
+ "effect": "^3.16.3",
16
+ "@effect/rpc": "^0.61.6"
15
17
  },
16
18
  "publishConfig": {
17
19
  "provenance": true
@@ -50,6 +52,16 @@
50
52
  "types": "./dist/dts/WorkflowEngine.d.ts",
51
53
  "import": "./dist/esm/WorkflowEngine.js",
52
54
  "default": "./dist/cjs/WorkflowEngine.js"
55
+ },
56
+ "./WorkflowProxy": {
57
+ "types": "./dist/dts/WorkflowProxy.d.ts",
58
+ "import": "./dist/esm/WorkflowProxy.js",
59
+ "default": "./dist/cjs/WorkflowProxy.js"
60
+ },
61
+ "./WorkflowProxyServer": {
62
+ "types": "./dist/dts/WorkflowProxyServer.d.ts",
63
+ "import": "./dist/esm/WorkflowProxyServer.js",
64
+ "default": "./dist/cjs/WorkflowProxyServer.js"
53
65
  }
54
66
  },
55
67
  "typesVersions": {
@@ -68,6 +80,12 @@
68
80
  ],
69
81
  "WorkflowEngine": [
70
82
  "./dist/dts/WorkflowEngine.d.ts"
83
+ ],
84
+ "WorkflowProxy": [
85
+ "./dist/dts/WorkflowProxy.d.ts"
86
+ ],
87
+ "WorkflowProxyServer": [
88
+ "./dist/dts/WorkflowProxyServer.d.ts"
71
89
  ]
72
90
  }
73
91
  }
package/src/Activity.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import type * as Cause from "effect/Cause"
5
4
  import * as Context from "effect/Context"
6
5
  import * as Effect from "effect/Effect"
7
6
  import * as Effectable from "effect/Effectable"
@@ -47,6 +46,11 @@ export interface Activity<
47
46
  Exit.Exit<Success["Encoded"], Error["Encoded"]>,
48
47
  Success["Context"] | Error["Context"]
49
48
  >
49
+ readonly execute: Effect.Effect<
50
+ Success["Type"],
51
+ Error["Type"],
52
+ Success["Context"] | Error["Context"] | R | WorkflowEngine | WorkflowInstance
53
+ >
50
54
  readonly executeEncoded: Effect.Effect<
51
55
  Success["Encoded"],
52
56
  Error["Encoded"],
@@ -95,6 +99,7 @@ export const make = <
95
99
  failure: errorSchema,
96
100
  defect: Schema.Defect
97
101
  }),
102
+ execute: options.execute,
98
103
  executeEncoded: Effect.matchEffect(options.execute, {
99
104
  onFailure: (error) => Effect.flatMap(Effect.orDie(Schema.encode(self.errorSchema as any)(error)), Effect.fail),
100
105
  onSuccess: (value) => Effect.orDie(Schema.encode(self.successSchema)(value))
@@ -122,39 +127,6 @@ export const retry: typeof Effect.retry = dual(
122
127
  })
123
128
  )
124
129
 
125
- /**
126
- * @since 1.0.0
127
- * @category Error handling
128
- */
129
- export const onError: {
130
- /**
131
- * @since 1.0.0
132
- * @category Error handling
133
- */
134
- <Error extends Schema.Schema.All, R2>(
135
- onError: (cause: Cause.Cause<Error["Type"]>) => Effect.Effect<void, never, R2>
136
- ): <Success extends Schema.Schema.Any, R>(
137
- self: Activity<Success, Error, R>
138
- ) => Activity<Success, Error, R | Exclude<R2, WorkflowEngine | WorkflowInstance>>
139
- /**
140
- * @since 1.0.0
141
- * @category Error handling
142
- */
143
- <Success extends Schema.Schema.Any, Error extends Schema.Schema.All, R, R2>(
144
- self: Activity<Success, Error, R>,
145
- onError: (cause: Cause.Cause<Error["Type"]>) => Effect.Effect<void, never, R2>
146
- ): Activity<Success, Error, R | Exclude<R2, WorkflowEngine | WorkflowInstance>>
147
- } = dual(2, <Success extends Schema.Schema.Any, Error extends Schema.Schema.All, R, R2>(
148
- self: Activity<Success, Error, R>,
149
- onError: (cause: Cause.Cause<Error["Type"]>) => Effect.Effect<void, never, R2>
150
- ): Activity<Success, Error, R | Exclude<R2, WorkflowEngine | WorkflowInstance>> =>
151
- make({
152
- name: `${self.name}/onError`,
153
- success: self.successSchema,
154
- error: self.errorSchema,
155
- execute: Effect.onError(self, onError)
156
- }))
157
-
158
130
  /**
159
131
  * @since 1.0.0
160
132
  * @category Attempts
package/src/Workflow.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
+ import type * as Cause from "effect/Cause"
4
5
  import * as Context from "effect/Context"
5
6
  import * as Data from "effect/Data"
6
7
  import * as Effect from "effect/Effect"
7
8
  import * as Exit from "effect/Exit"
9
+ import { dual } from "effect/Function"
8
10
  import * as Layer from "effect/Layer"
9
11
  import type { Pipeable } from "effect/Pipeable"
10
12
  import * as Predicate from "effect/Predicate"
@@ -12,6 +14,7 @@ import * as PrimaryKey from "effect/PrimaryKey"
12
14
  import * as Schedule from "effect/Schedule"
13
15
  import * as Schema from "effect/Schema"
14
16
  import type * as AST from "effect/SchemaAST"
17
+ import type * as Scope from "effect/Scope"
15
18
  import { makeHashDigest } from "./internal/crypto.js"
16
19
  import type { WorkflowEngine, WorkflowInstance } from "./WorkflowEngine.js"
17
20
 
@@ -42,6 +45,27 @@ export interface Workflow<
42
45
  readonly payloadSchema: Payload
43
46
  readonly successSchema: Success
44
47
  readonly errorSchema: Error
48
+ readonly annotations: Context.Context<never>
49
+
50
+ /**
51
+ * Add an annotation to the workflow.
52
+ */
53
+ annotate<I, S>(tag: Context.Tag<I, S>, value: S): Workflow<
54
+ Name,
55
+ Payload,
56
+ Success,
57
+ Error
58
+ >
59
+
60
+ /**
61
+ * Add the annotations from a Context object to the workflow.
62
+ */
63
+ annotateContext<I>(context: Context.Context<I>): Workflow<
64
+ Name,
65
+ Payload,
66
+ Success,
67
+ Error
68
+ >
45
69
 
46
70
  /**
47
71
  * Execute the workflow with the given payload.
@@ -76,7 +100,7 @@ export interface Workflow<
76
100
  Registration<Name> | WorkflowEngine,
77
101
  never,
78
102
  | WorkflowEngine
79
- | Exclude<R, WorkflowEngine | WorkflowInstance>
103
+ | Exclude<R, WorkflowEngine | WorkflowInstance | Execution<Name> | Scope.Scope>
80
104
  | Payload["Context"]
81
105
  | Success["Context"]
82
106
  | Error["Context"]
@@ -88,6 +112,26 @@ export interface Workflow<
88
112
  readonly executionId: (
89
113
  payload: Schema.Simplify<Schema.Struct.Constructor<Payload["fields"]>>
90
114
  ) => Effect.Effect<string>
115
+
116
+ /**
117
+ * Add compensation logic to an effect inside a Workflow. The compensation finalizer will be
118
+ * called if the entire workflow fails, allowing you to perform cleanup or
119
+ * other actions based on the success value and the cause of the workflow failure.
120
+ *
121
+ * NOTE: Compensation will not work for nested activities. Compensation
122
+ * finalizers are only registered for top-level effects in the workflow.
123
+ */
124
+ readonly withCompensation: {
125
+ <A, R2>(
126
+ compensation: (value: A, cause: Cause.Cause<Error["Type"]>) => Effect.Effect<void, never, R2>
127
+ ): <E, R>(
128
+ effect: Effect.Effect<A, E, R>
129
+ ) => Effect.Effect<A, E, R | R2 | WorkflowInstance | Execution<Name> | Scope.Scope>
130
+ <A, E, R, R2>(
131
+ effect: Effect.Effect<A, E, R>,
132
+ compensation: (value: A, cause: Cause.Cause<Error["Type"]>) => Effect.Effect<void, never, R2>
133
+ ): Effect.Effect<A, E, R | R2 | WorkflowInstance | Execution<Name> | Scope.Scope>
134
+ }
91
135
  }
92
136
 
93
137
  /**
@@ -124,6 +168,15 @@ export interface Registration<Name extends string> {
124
168
  readonly name: Name
125
169
  }
126
170
 
171
+ /**
172
+ * @since 1.0.0
173
+ * @category Models
174
+ */
175
+ export interface Execution<Name extends string> {
176
+ readonly _: unique symbol
177
+ readonly name: Name
178
+ }
179
+
127
180
  /**
128
181
  * @since 1.0.0
129
182
  * @category Models
@@ -134,9 +187,34 @@ export interface Any {
134
187
  readonly payloadSchema: AnyStructSchema
135
188
  readonly successSchema: Schema.Schema.Any
136
189
  readonly errorSchema: Schema.Schema.All
190
+ readonly annotations: Context.Context<never>
137
191
  readonly executionId: (payload: any) => Effect.Effect<string>
138
192
  }
139
193
 
194
+ /**
195
+ * @since 1.0.0
196
+ * @category Models
197
+ */
198
+ export type Registrations<Workflows extends Any> = Workflows extends Workflow<
199
+ infer _Name,
200
+ infer _Payload,
201
+ infer _Success,
202
+ infer _Error
203
+ > ? Registration<_Name> :
204
+ never
205
+
206
+ /**
207
+ * @since 1.0.0
208
+ * @category Models
209
+ */
210
+ export type Requirements<Workflows extends Any> = Workflows extends Workflow<
211
+ infer _Name,
212
+ infer _Payload,
213
+ infer _Success,
214
+ infer _Error
215
+ > ? _Payload["Context"] | _Success["Context"] | _Error["Context"] :
216
+ never
217
+
140
218
  const EngineTag = Context.GenericTag<WorkflowEngine, WorkflowEngine["Type"]>(
141
219
  "@effect/workflow/WorkflowEngine" satisfies typeof WorkflowEngine.key
142
220
  )
@@ -164,6 +242,7 @@ export const make = <
164
242
  readonly success?: Success
165
243
  readonly error?: Error
166
244
  readonly suspendedRetrySchedule?: Schedule.Schedule<any, unknown> | undefined
245
+ readonly annotations?: Context.Context<never>
167
246
  }
168
247
  ): Workflow<Name, Payload extends Schema.Struct.Fields ? Schema.Struct<Payload> : Payload, Success, Error> => {
169
248
  const suspendedRetrySchedule = options.suspendedRetrySchedule ?? defaultRetrySchedule
@@ -174,6 +253,19 @@ export const make = <
174
253
  payloadSchema: Schema.isSchema(options.payload) ? options.payload : Schema.Struct(options.payload as any),
175
254
  successSchema: options.success ?? Schema.Void as any,
176
255
  errorSchema: options.error ?? Schema.Never as any,
256
+ annotations: options.annotations ?? Context.empty(),
257
+ annotate(tag, value) {
258
+ return make({
259
+ ...options,
260
+ annotations: Context.add(self.annotations, tag, value)
261
+ })
262
+ },
263
+ annotateContext(context) {
264
+ return make({
265
+ ...options,
266
+ annotations: Context.merge(self.annotations, context)
267
+ })
268
+ },
177
269
  execute: Effect.fnUntraced(
178
270
  function*(fields: any, opts) {
179
271
  const payload = self.payloadSchema.make(fields)
@@ -229,7 +321,8 @@ export const make = <
229
321
  ) as any)
230
322
  return EngineTag.context(engine)
231
323
  })) as any,
232
- executionId: (payload) => makeExecutionId(self.payloadSchema.make(payload))
324
+ executionId: (payload) => makeExecutionId(self.payloadSchema.make(payload)),
325
+ withCompensation
233
326
  }
234
327
 
235
328
  return self
@@ -399,3 +492,63 @@ export const intoResult = <A, E, R>(
399
492
  })
400
493
  )
401
494
  )
495
+
496
+ /**
497
+ * Add compensation logic to an effect inside a Workflow. The compensation finalizer will be
498
+ * called if the entire workflow fails, allowing you to perform cleanup or
499
+ * other actions based on the success value and the cause of the workflow failure.
500
+ *
501
+ * NOTE: Compensation will not work for nested activities. Compensation
502
+ * finalizers are only registered for top-level effects in the workflow.
503
+ *
504
+ * @since 1.0.0
505
+ * @category Compensation
506
+ */
507
+ export const withCompensation: {
508
+ /**
509
+ * Add compensation logic to an effect inside a Workflow. The compensation finalizer will be
510
+ * called if the entire workflow fails, allowing you to perform cleanup or
511
+ * other actions based on the success value and the cause of the workflow failure.
512
+ *
513
+ * NOTE: Compensation will not work for nested activities. Compensation
514
+ * finalizers are only registered for top-level effects in the workflow.
515
+ *
516
+ * @since 1.0.0
517
+ * @category Compensation
518
+ */
519
+ <A, R2>(
520
+ compensation: (value: A, cause: Cause.Cause<unknown>) => Effect.Effect<void, never, R2>
521
+ ): <E, R>(
522
+ effect: Effect.Effect<A, E, R>
523
+ ) => Effect.Effect<A, E, R | R2 | WorkflowInstance | Scope.Scope>
524
+ /**
525
+ * Add compensation logic to an effect inside a Workflow. The compensation finalizer will be
526
+ * called if the entire workflow fails, allowing you to perform cleanup or
527
+ * other actions based on the success value and the cause of the workflow failure.
528
+ *
529
+ * NOTE: Compensation will not work for nested activities. Compensation
530
+ * finalizers are only registered for top-level effects in the workflow.
531
+ *
532
+ * @since 1.0.0
533
+ * @category Compensation
534
+ */
535
+ <A, E, R, R2>(
536
+ effect: Effect.Effect<A, E, R>,
537
+ compensation: (value: A, cause: Cause.Cause<unknown>) => Effect.Effect<void, never, R2>
538
+ ): Effect.Effect<A, E, R | R2 | WorkflowInstance | Scope.Scope>
539
+ } = dual(2, <A, E, R, R2>(
540
+ effect: Effect.Effect<A, E, R>,
541
+ compensation: (value: A, cause: Cause.Cause<unknown>) => Effect.Effect<void, never, R2>
542
+ ): Effect.Effect<A, E, R | R2 | WorkflowInstance | Scope.Scope> =>
543
+ Effect.uninterruptibleMask((restore) =>
544
+ Effect.contextWithEffect((context: Context.Context<WorkflowInstance>) => {
545
+ const instance = Context.get(context, InstanceTag)
546
+ return Effect.tap(restore(effect), (value) =>
547
+ Effect.addFinalizer((exit) => {
548
+ if (Exit.isSuccess(exit) || instance.suspended) {
549
+ return Effect.void
550
+ }
551
+ return compensation(value, exit.cause)
552
+ }))
553
+ })
554
+ ))
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import * as HttpApiEndpoint from "@effect/platform/HttpApiEndpoint"
5
+ import * as HttpApiGroup from "@effect/platform/HttpApiGroup"
6
+ import * as Rpc from "@effect/rpc/Rpc"
7
+ import * as RpcGroup from "@effect/rpc/RpcGroup"
8
+ import type { NonEmptyReadonlyArray } from "effect/Array"
9
+ import type * as Workflow from "./Workflow.js"
10
+
11
+ /**
12
+ * Derives an `RpcGroup` from a list of workflows.
13
+ *
14
+ * ```ts
15
+ * import { RpcServer } from "@effect/rpc"
16
+ * import { Workflow, WorkflowProxy, WorkflowProxyServer } from "@effect/workflow"
17
+ * import { Layer, Schema } from "effect"
18
+ *
19
+ * const EmailWorkflow = Workflow.make({
20
+ * name: "EmailWorkflow",
21
+ * payload: {
22
+ * id: Schema.String,
23
+ * to: Schema.String
24
+ * },
25
+ * idempotencyKey: ({ id }) => id
26
+ * })
27
+ *
28
+ * const myWorkflows = [EmailWorkflow] as const
29
+ *
30
+ * // Use WorkflowProxy.toRpcGroup to create a `RpcGroup` from the
31
+ * // workflows
32
+ * class MyRpcs extends WorkflowProxy.toRpcGroup(myWorkflows) {}
33
+ *
34
+ * // Use WorkflowProxyServer.layerRpcHandlers to create a layer that implements
35
+ * // the rpc handlers
36
+ * const ApiLayer = RpcServer.layer(MyRpcs).pipe(
37
+ * Layer.provide(WorkflowProxyServer.layerRpcHandlers(myWorkflows))
38
+ * )
39
+ * ```
40
+ *
41
+ * @since 1.0.0
42
+ * @category Constructors
43
+ */
44
+ export const toRpcGroup = <
45
+ const Workflows extends NonEmptyReadonlyArray<Workflow.Any>,
46
+ const Prefix extends string = ""
47
+ >(
48
+ workflows: Workflows,
49
+ options?: {
50
+ readonly prefix?: Prefix | undefined
51
+ }
52
+ ): RpcGroup.RpcGroup<ConvertRpcs<Workflows[number], Prefix>> => {
53
+ const prefix = options?.prefix ?? ""
54
+ const rpcs: Array<Rpc.Any> = []
55
+ for (const workflow of workflows) {
56
+ rpcs.push(
57
+ Rpc.make(`${prefix}${workflow.name}`, {
58
+ payload: workflow.payloadSchema,
59
+ error: workflow.errorSchema,
60
+ success: workflow.successSchema
61
+ }).annotateContext(workflow.annotations),
62
+ Rpc.make(`${prefix}${workflow.name}Discard`, {
63
+ payload: workflow.payloadSchema
64
+ }).annotateContext(workflow.annotations)
65
+ )
66
+ }
67
+ return RpcGroup.make(...rpcs) as any
68
+ }
69
+
70
+ /**
71
+ * @since 1.0.0
72
+ */
73
+ export type ConvertRpcs<Workflows extends Workflow.Any, Prefix extends string> = Workflows extends Workflow.Workflow<
74
+ infer _Name,
75
+ infer _Payload,
76
+ infer _Success,
77
+ infer _Error
78
+ > ?
79
+ | Rpc.Rpc<`${Prefix}${_Name}`, _Payload, _Success, _Error>
80
+ | Rpc.Rpc<`${Prefix}${_Name}Discard`, _Payload>
81
+ : never
82
+
83
+ /**
84
+ * Derives an `HttpApiGroup` from a list of workflows.
85
+ *
86
+ * ```ts
87
+ * import { HttpApi, HttpApiBuilder } from "@effect/platform"
88
+ * import { Workflow, WorkflowProxy, WorkflowProxyServer } from "@effect/workflow"
89
+ * import { Layer, Schema } from "effect"
90
+ *
91
+ * const EmailWorkflow = Workflow.make({
92
+ * name: "EmailWorkflow",
93
+ * payload: {
94
+ * id: Schema.String,
95
+ * to: Schema.String
96
+ * },
97
+ * idempotencyKey: ({ id }) => id
98
+ * })
99
+ *
100
+ * const myWorkflows = [EmailWorkflow] as const
101
+ *
102
+ * // Use WorkflowProxy.toHttpApiGroup to create a `HttpApiGroup` from the
103
+ * // workflows
104
+ * class MyApi extends HttpApi.make("api")
105
+ * .add(WorkflowProxy.toHttpApiGroup("workflows", myWorkflows))
106
+ * {}
107
+ *
108
+ * // Use WorkflowProxyServer.layerHttpApi to create a layer that implements the
109
+ * // workflows HttpApiGroup
110
+ * const ApiLayer = HttpApiBuilder.api(MyApi).pipe(
111
+ * Layer.provide(WorkflowProxyServer.layerHttpApi(MyApi, "workflows", myWorkflows))
112
+ * )
113
+ * ```
114
+ *
115
+ * @since 1.0.0
116
+ * @category Constructors
117
+ */
118
+ export const toHttpApiGroup = <const Name extends string, const Workflows extends NonEmptyReadonlyArray<Workflow.Any>>(
119
+ name: Name,
120
+ workflows: Workflows
121
+ ): HttpApiGroup.HttpApiGroup<Name, ConvertHttpApi<Workflows[number]>> => {
122
+ let group = HttpApiGroup.make(name)
123
+ for (const workflow of workflows) {
124
+ const path = `/${tagToPath(workflow.name)}` as const
125
+ group = group.add(
126
+ HttpApiEndpoint.post(workflow.name, path)
127
+ .setPayload(workflow.payloadSchema)
128
+ .addSuccess(workflow.successSchema)
129
+ .addError(workflow.errorSchema as any)
130
+ .annotateContext(workflow.annotations)
131
+ ).add(
132
+ HttpApiEndpoint.post(workflow.name + "Discard", `${path}/discard`)
133
+ .setPayload(workflow.payloadSchema)
134
+ .annotateContext(workflow.annotations)
135
+ ) as any
136
+ }
137
+ return group as any
138
+ }
139
+
140
+ const tagToPath = (tag: string): string =>
141
+ tag
142
+ .replace(/[^a-zA-Z0-9]+/g, "-") // Replace non-alphanumeric characters with hyphen
143
+ .replace(/([a-z])([A-Z])/g, "$1-$2") // Insert hyphen before uppercase letters
144
+ .toLowerCase()
145
+
146
+ /**
147
+ * @since 1.0.0
148
+ */
149
+ export type ConvertHttpApi<Workflows extends Workflow.Any> = Workflows extends Workflow.Workflow<
150
+ infer _Name,
151
+ infer _Payload,
152
+ infer _Success,
153
+ infer _Error
154
+ > ?
155
+ | HttpApiEndpoint.HttpApiEndpoint<
156
+ _Name,
157
+ "POST",
158
+ never,
159
+ never,
160
+ _Payload["Type"],
161
+ never,
162
+ _Success["Type"],
163
+ _Error["Type"],
164
+ _Payload["Context"] | _Success["Context"],
165
+ _Error["Context"]
166
+ >
167
+ | HttpApiEndpoint.HttpApiEndpoint<
168
+ `${_Name}Discard`,
169
+ "POST",
170
+ never,
171
+ never,
172
+ _Payload["Type"],
173
+ never,
174
+ void,
175
+ never,
176
+ _Payload["Context"]
177
+ > :
178
+ never