@reventlessdev/reventless-spec 3.0.0-alpha.61

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 (86) hide show
  1. package/CHANGELOG.md +625 -0
  2. package/LICENSE +202 -0
  3. package/README.md +114 -0
  4. package/package.json +34 -0
  5. package/rescript.json +25 -0
  6. package/run-generator.mjs +2 -0
  7. package/src/AnsiStyle.res +40 -0
  8. package/src/AnsiStyle.res.mjs +54 -0
  9. package/src/LogPrefix.res +168 -0
  10. package/src/LogPrefix.res.mjs +148 -0
  11. package/src/PackageVersion.res +73 -0
  12. package/src/PackageVersion.res.mjs +81 -0
  13. package/src/components/Aggregate.res +64 -0
  14. package/src/components/Aggregate.res.mjs +2 -0
  15. package/src/components/AutomationSlice.res +279 -0
  16. package/src/components/AutomationSlice.res.mjs +30 -0
  17. package/src/components/Counter.res +24 -0
  18. package/src/components/Counter.res.mjs +2 -0
  19. package/src/components/DcbDecode.res +128 -0
  20. package/src/components/DcbDecode.res.mjs +117 -0
  21. package/src/components/DcbTag.res +1256 -0
  22. package/src/components/DcbTag.res.mjs +869 -0
  23. package/src/components/DcbValidation.res +362 -0
  24. package/src/components/DcbValidation.res.mjs +360 -0
  25. package/src/components/DisplayName.res +40 -0
  26. package/src/components/DisplayName.res.mjs +26 -0
  27. package/src/components/ExtensionPoint.res +27 -0
  28. package/src/components/ExtensionPoint.res.mjs +2 -0
  29. package/src/components/InboundTranslationSlice.res +79 -0
  30. package/src/components/InboundTranslationSlice.res.mjs +2 -0
  31. package/src/components/OutboundTranslationSlice.res +108 -0
  32. package/src/components/OutboundTranslationSlice.res.mjs +2 -0
  33. package/src/components/Plugin.res +373 -0
  34. package/src/components/Plugin.res.mjs +249 -0
  35. package/src/components/ReadModel.res +199 -0
  36. package/src/components/ReadModel.res.mjs +18 -0
  37. package/src/components/Reference.res +51 -0
  38. package/src/components/Reference.res.mjs +37 -0
  39. package/src/components/StateAnnotations.res +65 -0
  40. package/src/components/StateAnnotations.res.mjs +15 -0
  41. package/src/components/StateChangeSlice.res +131 -0
  42. package/src/components/StateChangeSlice.res.mjs +2 -0
  43. package/src/components/StateViewSlice.res +99 -0
  44. package/src/components/StateViewSlice.res.mjs +2 -0
  45. package/src/components/Task.res +62 -0
  46. package/src/components/Task.res.mjs +2 -0
  47. package/src/generator/Codegen.res +703 -0
  48. package/src/generator/Codegen.res.mjs +485 -0
  49. package/src/generator/Config.res +85 -0
  50. package/src/generator/Config.res.mjs +78 -0
  51. package/src/generator/Discovery.res +204 -0
  52. package/src/generator/Discovery.res.mjs +213 -0
  53. package/src/generator/Generator_Node.res +49 -0
  54. package/src/generator/Generator_Node.res.mjs +18 -0
  55. package/src/generator/Pairing.res +386 -0
  56. package/src/generator/Pairing.res.mjs +353 -0
  57. package/src/generator/PluginGenerator.res +53 -0
  58. package/src/generator/PluginGenerator.res.mjs +70 -0
  59. package/src/types/Authorization.res +23 -0
  60. package/src/types/Authorization.res.mjs +33 -0
  61. package/src/types/Behavior.res +78 -0
  62. package/src/types/Behavior.res.mjs +2 -0
  63. package/src/types/EventMapping.res +100 -0
  64. package/src/types/EventMapping.res.mjs +15 -0
  65. package/src/types/Handler.res +30 -0
  66. package/src/types/Handler.res.mjs +2 -0
  67. package/src/types/Id.res +75 -0
  68. package/src/types/Id.res.mjs +37 -0
  69. package/src/types/Identity.res +46 -0
  70. package/src/types/Identity.res.mjs +51 -0
  71. package/src/types/Message.res +197 -0
  72. package/src/types/Message.res.mjs +108 -0
  73. package/src/types/Projection.res +220 -0
  74. package/src/types/Projection.res.mjs +44 -0
  75. package/src/types/QueryEngine.res +123 -0
  76. package/src/types/QueryEngine.res.mjs +12 -0
  77. package/src/types/ReadConsistency.res +38 -0
  78. package/src/types/ReadConsistency.res.mjs +29 -0
  79. package/src/types/Schedule.res +65 -0
  80. package/src/types/Schedule.res.mjs +68 -0
  81. package/src/types/SideEffect.res +45 -0
  82. package/src/types/SideEffect.res.mjs +2 -0
  83. package/src/types/StoredEvent.res +46 -0
  84. package/src/types/StoredEvent.res.mjs +32 -0
  85. package/src/types/Visibility.res +24 -0
  86. package/src/types/Visibility.res.mjs +25 -0
@@ -0,0 +1,148 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
7
+ import * as AnsiStyle$Reventless from "./AnsiStyle.res.mjs";
8
+
9
+ let currentPluginName = {
10
+ contents: undefined
11
+ };
12
+
13
+ let componentPluginRegistry = {
14
+ contents: {}
15
+ };
16
+
17
+ function registerComponentPlugin(componentName, pluginName) {
18
+ componentPluginRegistry.contents[componentName] = pluginName;
19
+ }
20
+
21
+ function hashStr(s) {
22
+ let h = 0;
23
+ for (let i = 0, i_finish = s.length; i < i_finish; ++i) {
24
+ let code = Stdlib_Option.getOr(Stdlib_String.charCodeAt(s, i), 0);
25
+ h = h + (code * (i + 1 | 0) | 0) | 0;
26
+ }
27
+ return h;
28
+ }
29
+
30
+ let pluginColors = [
31
+ "\x1b[32m",
32
+ "\x1b[34m",
33
+ "\x1b[35m",
34
+ "\x1b[92m",
35
+ "\x1b[94m",
36
+ "\x1b[95m"
37
+ ];
38
+
39
+ function pluginColor(name) {
40
+ return pluginColors[Primitive_int.mod_(hashStr(name), pluginColors.length)];
41
+ }
42
+
43
+ function extractInnerName(comp) {
44
+ let openIdx = comp.indexOf("(");
45
+ let closeIdx = comp.lastIndexOf(")");
46
+ if (openIdx > 0 && closeIdx > (openIdx + 1 | 0)) {
47
+ return comp.slice(openIdx + 1 | 0, closeIdx);
48
+ }
49
+ }
50
+
51
+ function stripAtVersion(s) {
52
+ let idx = s.indexOf("@");
53
+ if (idx > 0) {
54
+ return s.slice(0, idx);
55
+ }
56
+ }
57
+
58
+ function stripPluginSuffix(s) {
59
+ let suffix = "Plugin";
60
+ if (s.endsWith(suffix) && s.length > suffix.length) {
61
+ return s.slice(0, s.length - suffix.length | 0);
62
+ }
63
+ }
64
+
65
+ function firstDotSegment(s) {
66
+ let idx = s.indexOf(".");
67
+ if (idx > 0) {
68
+ return s.slice(0, idx);
69
+ }
70
+ }
71
+
72
+ function lastDotSegment(s) {
73
+ let idx = s.lastIndexOf(".");
74
+ if (idx > 0 && (idx + 1 | 0) < s.length) {
75
+ return s.slice(idx + 1 | 0, s.length);
76
+ }
77
+ }
78
+
79
+ function lookup(name) {
80
+ return componentPluginRegistry.contents[name];
81
+ }
82
+
83
+ function resolvePlugin(comp, param) {
84
+ if (comp === undefined) {
85
+ return currentPluginName.contents;
86
+ }
87
+ let inner = extractInnerName(comp);
88
+ if (inner === undefined) {
89
+ return currentPluginName.contents;
90
+ }
91
+ let p = componentPluginRegistry.contents[inner];
92
+ if (p !== undefined) {
93
+ return p;
94
+ }
95
+ let p$1 = currentPluginName.contents;
96
+ if (p$1 !== undefined) {
97
+ return p$1;
98
+ }
99
+ let candidates = [
100
+ stripAtVersion(inner),
101
+ stripPluginSuffix(inner),
102
+ lastDotSegment(inner),
103
+ firstDotSegment(inner)
104
+ ];
105
+ return Stdlib_Array.reduce(candidates, undefined, (acc, candidate) => {
106
+ if (acc !== undefined) {
107
+ return acc;
108
+ } else {
109
+ return Stdlib_Option.flatMap(candidate, lookup);
110
+ }
111
+ });
112
+ }
113
+
114
+ function fmtPlainPrefix(comp, param) {
115
+ let p = resolvePlugin(comp, undefined);
116
+ let plugin = p !== undefined ? `[` + p + `]` : "";
117
+ let c = comp !== undefined ? `[` + comp + `] ` : "";
118
+ return plugin + c;
119
+ }
120
+
121
+ function fmtComp(comp, param) {
122
+ if (!AnsiStyle$Reventless.useAnsi()) {
123
+ return fmtPlainPrefix(comp, undefined);
124
+ }
125
+ let p = resolvePlugin(comp, undefined);
126
+ let plugin = p !== undefined ? pluginColor(p) + AnsiStyle$Reventless.bold(`[` + p + `]`) : "";
127
+ let c = comp !== undefined ? AnsiStyle$Reventless.bold(`[` + comp + `]`) + ` ` : "";
128
+ return plugin + c;
129
+ }
130
+
131
+ export {
132
+ currentPluginName,
133
+ componentPluginRegistry,
134
+ registerComponentPlugin,
135
+ hashStr,
136
+ pluginColors,
137
+ pluginColor,
138
+ extractInnerName,
139
+ stripAtVersion,
140
+ stripPluginSuffix,
141
+ firstDotSegment,
142
+ lastDotSegment,
143
+ lookup,
144
+ resolvePlugin,
145
+ fmtPlainPrefix,
146
+ fmtComp,
147
+ }
148
+ /* AnsiStyle-Reventless Not a pure module */
@@ -0,0 +1,73 @@
1
+ @module("node:fs") external readFileSync: (string, string) => string = "readFileSync"
2
+ @module("node:fs") external existsSync: string => bool = "existsSync"
3
+ @module("node:url") external fileURLToPath: string => string = "fileURLToPath"
4
+ @module("node:path") external dirname: string => string = "dirname"
5
+ @module("node:path") external join: (string, string) => string = "join"
6
+
7
+ let readVersion = (path: string) =>
8
+ readFileSync(path, "utf8")
9
+ ->JSON.parseOrThrow
10
+ ->JSON.Decode.object
11
+ ->Option.flatMap(o => o->Dict.get("version"))
12
+ ->Option.flatMap(JSON.Decode.string)
13
+ ->Option.getOr("0.0.0")
14
+
15
+ let fromCwd = () => readVersion("./package.json")
16
+
17
+ let findVersion = dir => {
18
+ let rec find = dir => {
19
+ let candidate = join(dir, "package.json")
20
+ if existsSync(candidate) {
21
+ readVersion(candidate)
22
+ } else {
23
+ let parent = dirname(dir)
24
+ if parent == dir {
25
+ "0.0.0"
26
+ } else {
27
+ find(parent)
28
+ }
29
+ }
30
+ }
31
+ find(dir)
32
+ }
33
+
34
+ let fromModuleUrl = (url: string) => findVersion(url->fileURLToPath->dirname)
35
+
36
+ /** Detect the caller's file via V8 Structured Stack Trace API and read the
37
+ version from the nearest package.json. Skips frames from framework packages
38
+ (reventless-spec, reventless-core, reventless-infra, reventless-local,
39
+ reventless-aws, reventless-interop). Falls back to CWD's package.json. */
40
+ let callerFile: unit => option<string> = %raw(`
41
+ function() {
42
+ var frameworkMarkers = [
43
+ '/reventless/reventless-',
44
+ '/@reventlessdev/reventless-',
45
+ ];
46
+ var oldPrepare = Error.prepareStackTrace;
47
+ Error.prepareStackTrace = function(_, stack) { return stack; };
48
+ var err = new Error();
49
+ var stack = err.stack;
50
+ Error.prepareStackTrace = oldPrepare;
51
+ for (var i = 0; i < stack.length; i++) {
52
+ var file = stack[i].getFileName();
53
+ if (!file) continue;
54
+ var isFramework = false;
55
+ for (var j = 0; j < frameworkMarkers.length; j++) {
56
+ if (file.indexOf(frameworkMarkers[j]) !== -1) { isFramework = true; break; }
57
+ }
58
+ if (!isFramework) return file;
59
+ }
60
+ return undefined;
61
+ }
62
+ `)
63
+
64
+ let fromCaller = () =>
65
+ switch callerFile() {
66
+ | Some(file) =>
67
+ if file->String.startsWith("file://") {
68
+ findVersion(file->fileURLToPath->dirname)
69
+ } else {
70
+ findVersion(file->dirname)
71
+ }
72
+ | None => fromCwd()
73
+ }
@@ -0,0 +1,81 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodeurl from "node:url";
5
+ import * as Nodepath from "node:path";
6
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+
9
+ function readVersion(path) {
10
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(Nodefs.readFileSync(path, "utf8"))), o => o["version"]), Stdlib_JSON.Decode.string), "0.0.0");
11
+ }
12
+
13
+ function fromCwd() {
14
+ return readVersion("./package.json");
15
+ }
16
+
17
+ function findVersion(dir) {
18
+ let _dir = dir;
19
+ while (true) {
20
+ let dir$1 = _dir;
21
+ let candidate = Nodepath.join(dir$1, "package.json");
22
+ if (Nodefs.existsSync(candidate)) {
23
+ return readVersion(candidate);
24
+ }
25
+ let parent = Nodepath.dirname(dir$1);
26
+ if (parent === dir$1) {
27
+ return "0.0.0";
28
+ }
29
+ _dir = parent;
30
+ continue;
31
+ };
32
+ }
33
+
34
+ function fromModuleUrl(url) {
35
+ return findVersion(Nodepath.dirname(Nodeurl.fileURLToPath(url)));
36
+ }
37
+
38
+ let callerFile = (function() {
39
+ var frameworkMarkers = [
40
+ '/reventless/reventless-',
41
+ '/@reventlessdev/reventless-',
42
+ ];
43
+ var oldPrepare = Error.prepareStackTrace;
44
+ Error.prepareStackTrace = function(_, stack) { return stack; };
45
+ var err = new Error();
46
+ var stack = err.stack;
47
+ Error.prepareStackTrace = oldPrepare;
48
+ for (var i = 0; i < stack.length; i++) {
49
+ var file = stack[i].getFileName();
50
+ if (!file) continue;
51
+ var isFramework = false;
52
+ for (var j = 0; j < frameworkMarkers.length; j++) {
53
+ if (file.indexOf(frameworkMarkers[j]) !== -1) { isFramework = true; break; }
54
+ }
55
+ if (!isFramework) return file;
56
+ }
57
+ return undefined;
58
+ });
59
+
60
+ function fromCaller() {
61
+ let file = callerFile();
62
+ if (file !== undefined) {
63
+ if (file.startsWith("file://")) {
64
+ return findVersion(Nodepath.dirname(Nodeurl.fileURLToPath(file)));
65
+ } else {
66
+ return findVersion(Nodepath.dirname(file));
67
+ }
68
+ } else {
69
+ return readVersion("./package.json");
70
+ }
71
+ }
72
+
73
+ export {
74
+ readVersion,
75
+ fromCwd,
76
+ findVersion,
77
+ fromModuleUrl,
78
+ callerFile,
79
+ fromCaller,
80
+ }
81
+ /* node:fs Not a pure module */
@@ -0,0 +1,64 @@
1
+ /**
2
+ Module type for an aggregate's identity and schema specification.
3
+
4
+ Every aggregate defines a `Spec` module satisfying this type. The `Spec` is
5
+ used by `Platform.Aggregate.Make` and by `Behavior.T` to fix the aggregate's
6
+ command, event, and error types.
7
+
8
+ @example
9
+ ```rescript
10
+ // Category.res
11
+ module Id = Id.String
12
+ let name = "Category"
13
+
14
+ @schema
15
+ type command =
16
+ | AddCategory({categoryId: string, name: string})
17
+ | RenameCategory({categoryId: string, name: string})
18
+ | ArchiveCategory({categoryId: string})
19
+
20
+ @schema
21
+ type event =
22
+ | CategoryAdded({categoryId: string, name: string})
23
+ | CategoryRenamed({categoryId: string, name: string})
24
+ | CategoryArchived({categoryId: string})
25
+
26
+ @schema
27
+ type error =
28
+ | CategoryAlreadyExists
29
+ | CategoryNotFound
30
+ | CategoryAlreadyArchived
31
+ ```
32
+ */
33
+ module type Spec = {
34
+ module Id: Id.T
35
+
36
+ /** Logical aggregate name used as a prefix for infrastructure resource names. */
37
+ let name: string
38
+
39
+ /** Commands this aggregate accepts. Must carry `@schema`. */
40
+ @schema
41
+ type command
42
+
43
+ /** Events this aggregate emits. Must carry `@schema`. */
44
+ @schema
45
+ type event
46
+
47
+ /** Business rule violation errors. Must carry `@schema`. */
48
+ @schema
49
+ type error
50
+
51
+ /** Sury schema for the command type — generated automatically by `@schema`. */
52
+ let commandSchema: S.t<command>
53
+
54
+ /** File URL of this module (`import.meta.url`). Used by AWS builders to derive
55
+ the npm specifier for runtime dynamic imports. */
56
+ let moduleUrl: string
57
+
58
+ /** Authorization rule evaluated at the GraphQL resolver entry before any
59
+ command is dispatched. Auto-injected by `@@reventless.spec` and on
60
+ structurally-detected inline spec modules — defaults to
61
+ `AllowAuthenticated`; override at the file/module level with
62
+ `@@reventless.authorize(<rule>)`. */
63
+ let commandAuthorization: command => Authorization.permission
64
+ }
@@ -0,0 +1,2 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+ /* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */
@@ -0,0 +1,279 @@
1
+ /**
2
+ Module types for a DCB automation slice (TODO List Pattern).
3
+
4
+ An `AutomationSlice` listens to the shared `DcbEventLog` event topic, accumulates
5
+ pending work items (TODO list), and processes them exactly once by issuing commands.
6
+ Completion events mark TODO items as done, closing the automation loop.
7
+
8
+ ```
9
+ Event(s) → TODO List (read model) → Processor → Command → Event(s)
10
+ ```
11
+
12
+ Plan 02 splits the merged spec into two module types:
13
+
14
+ - `Spec` — types, identity, schemas, configuration constants. Per D2,
15
+ `todoItem` lives here (the persisted TODO state has a schema queried for
16
+ sweeping pending items — externally-observable, like ReadModel state).
17
+ - `Automation` — the three pure functions `collect`, `resolve`, `process`.
18
+
19
+ @example
20
+ ```rescript
21
+ // ShipOrder.res
22
+ let name = "ShipOrder"
23
+
24
+ @schema type todoItem = {orderId: string, shippingAddress: string}
25
+ @schema type command = CreateShipment({orderId: @s.matches(DcbTag.string) string, address: string})
26
+
27
+ let collect = event => switch event {
28
+ | OrderPlaced({orderId, shippingAddress}) =>
29
+ [(orderId, {orderId, shippingAddress})]
30
+ | _ => []
31
+ }
32
+
33
+ let resolve = event => switch event {
34
+ | ShipmentCreated({orderId}) => Some(orderId)
35
+ | _ => None
36
+ }
37
+
38
+ let process = (id, item) =>
39
+ Some((id, CreateShipment({orderId: item.orderId, address: item.shippingAddress})))
40
+
41
+ let maxRetries = 3
42
+ let heartbeatInterval = 60
43
+ ```
44
+ */
45
+
46
+ /**
47
+ The lean Spec for an AutomationSlice — types, identity, schemas, sweep config.
48
+
49
+ Plan 04 dropped `consumedEvent` from the Spec — the framework now derives the
50
+ consumed-event set from each per-source Mapping's `sourceEventSchema`, so a
51
+ manually-declared union is no longer necessary. Single-source slices have one
52
+ `Mapping` whose `sourceEventSchema` IS the consumed-event schema; multi-source
53
+ slices have several Mappings and the framework walks all of them.
54
+ */
55
+ module type Spec = {
56
+ /** Logical name of this automation slice (used as a component prefix). */
57
+ let name: string
58
+ let moduleUrl: string
59
+
60
+ /** The TODO item state — what data is accumulated for each pending work item. Must carry `@schema`. */
61
+ @schema
62
+ type todoItem
63
+
64
+ /** The command type produced by the processor. Must carry `@schema`. */
65
+ @schema
66
+ type command
67
+
68
+ /** Maximum number of retries for a failed processing attempt. */
69
+ let maxRetries: int
70
+
71
+ /** Heartbeat interval in seconds for sweeping pending/failed items. */
72
+ let heartbeatInterval: int
73
+
74
+ /** Name of the aggregate or StateChangeSlice that receives the produced command. */
75
+ let targetName: string
76
+ }
77
+
78
+ // ── Plan 04: Mixed-source mappings ───────────────────────────────────────────
79
+ //
80
+ // An AutomationSlice can declare per-source mappings that consume Aggregate
81
+ // events alongside the slice's own DcbEventLog events. Each mapping carries its
82
+ // own `collect`/`resolve` (over the source's `event` type) plus a `toTags`
83
+ // validation step run before each command publish. `process` remains shared in
84
+ // the slice's `Automation` module since it operates on `todoItem` regardless of
85
+ // the originating source.
86
+
87
+ /**
88
+ Ambient deployment context plumbed to every mapping function.
89
+
90
+ `Plugin_Builder` constructs this from its existing `~environment`, `~name` and
91
+ related parameters. `collect` and `resolve` use it to complete partial event
92
+ payloads (e.g. when a DCB tag field is supplied by deployment metadata rather
93
+ than the source event); `toTags` uses it to validate that all tag fields on the
94
+ target command will be populated.
95
+
96
+ The record is intentionally narrow — extending it is a deliberate framework
97
+ change, not an open-dict escape hatch. Runtime registry lookups are out of
98
+ scope (see Plan 04 / decision 3).
99
+ */
100
+ type context = {
101
+ environment: string,
102
+ platformName: string,
103
+ pluginName: string,
104
+ sliceName: string,
105
+ }
106
+
107
+ /**
108
+ A compiled per-source-to-AutomationSlice mapping.
109
+
110
+ Created by `AutomationSlice.Mapping.Make(Source, Target, Impl)`. The mapping
111
+ binds `sourceEvent` (decode target) plus `collect`/`resolve` to the slice's
112
+ `todoItem` type. `process` remains in the slice's `Automation` module — it is
113
+ source-agnostic.
114
+
115
+ Validation belongs upstream of the mapping: `collect` should return `[]` for
116
+ events that shouldn't enter the TODO list, and the command schema's
117
+ `@compositePartitionTag` / `@s.matches(DcbTag.string)` annotations enforce
118
+ DCB-tag invariants at encode time (failures mark the item Failed → retry).
119
+ */
120
+ module type Mapping = {
121
+ module SourceId: Id.T
122
+ @schema
123
+ type sourceEvent
124
+ type todoItem
125
+ type command
126
+ let sourceEventSchema: S.t<sourceEvent>
127
+ let sourceName: string
128
+ let collect: (sourceEvent, context) => array<(string, todoItem)>
129
+ let resolve: sourceEvent => option<string>
130
+ }
131
+
132
+ /**
133
+ The Automation — the source-agnostic processing function.
134
+
135
+ Plan 04 moved `collect` and `resolve` to per-source `Mapping` modules (where
136
+ they switch over the source's own `event` type). `process` stays here because
137
+ it operates on `todoItem`, which is uniform across sources.
138
+
139
+ The "close PPX gaps" plan merged the previous `Mappings` shape into this type:
140
+ the same module that exposes `process` also exposes the per-source `mappings`
141
+ array and the inner `module type Mapping`. This collapses the Plan 04 3-arg
142
+ `AutomationSlice.Make(Spec, Automation, Mappings)` into a 2-arg
143
+ `Make(Spec, Automation)` and lets the merged `_Automation.res` file (process +
144
+ per-source `Mapping.Make` + `let mappings`) live as a single source file.
145
+ */
146
+ module type Automation = {
147
+ module Spec: Spec
148
+
149
+ /**
150
+ Process: given a pending TODO item, produce a command.
151
+ The processor calls this for each pending item. Returns the target ID and command.
152
+ May return `None` to skip processing (e.g., wait for more data).
153
+ */
154
+ let process: (string, Spec.todoItem) => option<(string, Spec.command)>
155
+
156
+ /** File URL of this Automation module (`import.meta.url`). */
157
+ let moduleUrl: string
158
+
159
+ /**
160
+ The per-source `Mapping` type — substituted onto the slice's `Spec`. The
161
+ PPX-injected `module M = Reventless.AutomationSlice.Mappings.Make(Spec)`
162
+ + `module type Mapping = M.Mapping` produces exactly this shape; legacy
163
+ 3-file files re-export it via `module type Mapping = <Stem>_Mappings.Mapping`.
164
+ */
165
+ module type Mapping = Mapping
166
+ with type todoItem = Spec.todoItem
167
+ and type command = Spec.command
168
+
169
+ /**
170
+ The per-source mappings registered with this slice. Each entry is a first-class
171
+ module satisfying `Mapping`. The framework walks these to discover source
172
+ topics and dispatch decoded events to the matching `collect`/`resolve`.
173
+ */
174
+ let mappings: array<module(Mapping)>
175
+ }
176
+
177
+ /**
178
+ The implementation passed to `AutomationSlice.Mapping.Make`. Caller-supplied
179
+ `collect`/`resolve` are bound to source/target types via destructive
180
+ substitution in the functor.
181
+ */
182
+ module type MappingImpl = {
183
+ type sourceEvent
184
+ type todoItem
185
+ type command
186
+ let collect: (sourceEvent, context) => array<(string, todoItem)>
187
+ let resolve: sourceEvent => option<string>
188
+ }
189
+
190
+ /**
191
+ A collection of `Mapping` modules for a single AutomationSlice target.
192
+
193
+ Pass a `Mappings` module to the AutomationSlice plugin builder to register all
194
+ source-to-slice mappings. The shape mirrors `Projection.Mappings` for
195
+ ReadModels.
196
+
197
+ @example
198
+ ```rescript
199
+ // AutoFulfillment_Mappings.res
200
+ module M = Reventless.AutomationSlice.Mappings.Make(AutoFulfillmentSpec)
201
+ module type Mapping = M.Mapping
202
+
203
+ let mappings: array<module(Mapping)> = [
204
+ module(FromOrderShipped),
205
+ module(FromStockReserved),
206
+ ]
207
+ ```
208
+ */
209
+ module type Mappings = {
210
+ module Target: Spec
211
+ module type Mapping = Mapping
212
+ with type todoItem = Target.todoItem
213
+ and type command = Target.command
214
+ let moduleUrl: string
215
+ let mappings: array<module(Mapping)>
216
+ }
217
+
218
+ /**
219
+ Builds an `AutomationSlice.Mapping` from a `Source`, the slice's `Spec`, and a
220
+ `MappingImpl`.
221
+
222
+ @example
223
+ ```rescript
224
+ // AutoFulfillment/FromOrderShipped.res
225
+ module FromOrderShipped = Reventless.AutomationSlice.Mapping.Make(
226
+ OrderSpec, // Source: Aggregate spec module
227
+ AutoFulfillmentSpec, // Target: this slice's spec
228
+ {
229
+ let collect = (event, _ctx) =>
230
+ switch event {
231
+ | OrderSpec.OrderShipped({orderId, productId}) =>
232
+ [(orderId ++ ":" ++ productId, {AutoFulfillmentSpec.orderId, productId})]
233
+ | _ => []
234
+ }
235
+
236
+ let resolve = event =>
237
+ switch event {
238
+ | OrderSpec.OrderRefunded({orderId, productId}) =>
239
+ Some(orderId ++ ":" ++ productId)
240
+ | _ => None
241
+ }
242
+ },
243
+ )
244
+ ```
245
+ */
246
+ module Mapping = {
247
+ module Make = (
248
+ Source: Projection.Source,
249
+ Target: Spec,
250
+ Impl: MappingImpl
251
+ with type sourceEvent := Source.event
252
+ and type todoItem := Target.todoItem
253
+ and type command := Target.command,
254
+ ): (
255
+ Mapping
256
+ with type sourceEvent = Source.event
257
+ and type todoItem = Target.todoItem
258
+ and type command = Target.command
259
+ and module SourceId = Source.Id
260
+ ) => {
261
+ module SourceId = Source.Id
262
+ @schema
263
+ type sourceEvent = Source.event
264
+ type todoItem = Target.todoItem
265
+ type command = Target.command
266
+ let sourceName = Source.name
267
+ let collect = Impl.collect
268
+ let resolve = Impl.resolve
269
+ }
270
+ }
271
+
272
+ module Mappings = {
273
+ module Make = (Target: Spec) => {
274
+ module type Mapping = Mapping
275
+ with type todoItem = Target.todoItem
276
+ and type command = Target.command
277
+ }
278
+ }
279
+
@@ -0,0 +1,30 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ function Make(Source) {
5
+ return Target => (Impl => ({
6
+ SourceId: Source.Id,
7
+ sourceEventSchema: Source.eventSchema,
8
+ sourceName: Source.name,
9
+ collect: Impl.collect,
10
+ resolve: Impl.resolve
11
+ }));
12
+ }
13
+
14
+ let Mapping = {
15
+ Make: Make
16
+ };
17
+
18
+ function Make$1(Target) {
19
+ return {};
20
+ }
21
+
22
+ let Mappings = {
23
+ Make: Make$1
24
+ };
25
+
26
+ export {
27
+ Mapping,
28
+ Mappings,
29
+ }
30
+ /* No side effect */
@@ -0,0 +1,24 @@
1
+ /**
2
+ A named counter (e.g. `"ProductCount"`). Used as a key in the counts database.
3
+ Multiple counters can exist within the same `Counter` component.
4
+ */
5
+ type counterId = string
6
+
7
+ /**
8
+ An opaque reference string that de-duplicates counter increments.
9
+
10
+ Each unique `reference` is counted at most once per `counterId`.
11
+ Typically set to the event's message ID to make increments idempotent.
12
+ */
13
+ type reference = string
14
+
15
+ /**
16
+ A counter threshold target.
17
+
18
+ When the count for `counterId` reaches `target`, the event mapping
19
+ framework fires the associated command.
20
+ */
21
+ type counterTarget = {
22
+ counterId: counterId,
23
+ target: int,
24
+ }