@hotmeshio/hotmesh 0.0.1

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 (263) hide show
  1. package/LICENSE +214 -0
  2. package/README.md +241 -0
  3. package/build/index.d.ts +4 -0
  4. package/build/index.js +7 -0
  5. package/build/modules/errors.d.ts +28 -0
  6. package/build/modules/errors.js +50 -0
  7. package/build/modules/key.d.ts +75 -0
  8. package/build/modules/key.js +116 -0
  9. package/build/modules/utils.d.ts +34 -0
  10. package/build/modules/utils.js +173 -0
  11. package/build/package.json +73 -0
  12. package/build/services/activities/activity.d.ts +59 -0
  13. package/build/services/activities/activity.js +396 -0
  14. package/build/services/activities/await.d.ts +16 -0
  15. package/build/services/activities/await.js +143 -0
  16. package/build/services/activities/emit.d.ts +9 -0
  17. package/build/services/activities/emit.js +13 -0
  18. package/build/services/activities/index.d.ts +15 -0
  19. package/build/services/activities/index.js +16 -0
  20. package/build/services/activities/iterate.d.ts +9 -0
  21. package/build/services/activities/iterate.js +13 -0
  22. package/build/services/activities/trigger.d.ts +22 -0
  23. package/build/services/activities/trigger.js +161 -0
  24. package/build/services/activities/worker.d.ts +17 -0
  25. package/build/services/activities/worker.js +164 -0
  26. package/build/services/collator/index.d.ts +54 -0
  27. package/build/services/collator/index.js +171 -0
  28. package/build/services/compiler/deployer.d.ts +35 -0
  29. package/build/services/compiler/deployer.js +412 -0
  30. package/build/services/compiler/index.d.ts +30 -0
  31. package/build/services/compiler/index.js +111 -0
  32. package/build/services/compiler/validator.d.ts +32 -0
  33. package/build/services/compiler/validator.js +134 -0
  34. package/build/services/connector/clients/ioredis.d.ts +13 -0
  35. package/build/services/connector/clients/ioredis.js +50 -0
  36. package/build/services/connector/clients/redis.d.ts +13 -0
  37. package/build/services/connector/clients/redis.js +62 -0
  38. package/build/services/connector/index.d.ts +5 -0
  39. package/build/services/connector/index.js +31 -0
  40. package/build/services/dimension/index.d.ts +29 -0
  41. package/build/services/dimension/index.js +35 -0
  42. package/build/services/durable/asyncLocalStorage.d.ts +3 -0
  43. package/build/services/durable/asyncLocalStorage.js +5 -0
  44. package/build/services/durable/client.d.ts +15 -0
  45. package/build/services/durable/client.js +108 -0
  46. package/build/services/durable/connection.d.ts +4 -0
  47. package/build/services/durable/connection.js +51 -0
  48. package/build/services/durable/factory.d.ts +3 -0
  49. package/build/services/durable/factory.js +123 -0
  50. package/build/services/durable/handle.d.ts +8 -0
  51. package/build/services/durable/handle.js +38 -0
  52. package/build/services/durable/index.d.ts +57 -0
  53. package/build/services/durable/index.js +58 -0
  54. package/build/services/durable/native.d.ts +4 -0
  55. package/build/services/durable/native.js +47 -0
  56. package/build/services/durable/worker.d.ts +36 -0
  57. package/build/services/durable/worker.js +266 -0
  58. package/build/services/durable/workflow.d.ts +6 -0
  59. package/build/services/durable/workflow.js +135 -0
  60. package/build/services/engine/index.d.ts +82 -0
  61. package/build/services/engine/index.js +511 -0
  62. package/build/services/hotmesh/index.d.ts +45 -0
  63. package/build/services/hotmesh/index.js +134 -0
  64. package/build/services/logger/index.d.ts +17 -0
  65. package/build/services/logger/index.js +73 -0
  66. package/build/services/mapper/index.d.ts +24 -0
  67. package/build/services/mapper/index.js +72 -0
  68. package/build/services/pipe/functions/array.d.ts +24 -0
  69. package/build/services/pipe/functions/array.js +69 -0
  70. package/build/services/pipe/functions/bitwise.d.ts +9 -0
  71. package/build/services/pipe/functions/bitwise.js +24 -0
  72. package/build/services/pipe/functions/conditional.d.ts +10 -0
  73. package/build/services/pipe/functions/conditional.js +27 -0
  74. package/build/services/pipe/functions/date.d.ts +57 -0
  75. package/build/services/pipe/functions/date.js +167 -0
  76. package/build/services/pipe/functions/index.d.ts +25 -0
  77. package/build/services/pipe/functions/index.js +26 -0
  78. package/build/services/pipe/functions/json.d.ts +5 -0
  79. package/build/services/pipe/functions/json.js +12 -0
  80. package/build/services/pipe/functions/math.d.ts +38 -0
  81. package/build/services/pipe/functions/math.js +111 -0
  82. package/build/services/pipe/functions/number.d.ts +25 -0
  83. package/build/services/pipe/functions/number.js +133 -0
  84. package/build/services/pipe/functions/object.d.ts +22 -0
  85. package/build/services/pipe/functions/object.js +63 -0
  86. package/build/services/pipe/functions/string.d.ts +23 -0
  87. package/build/services/pipe/functions/string.js +69 -0
  88. package/build/services/pipe/functions/symbol.d.ts +12 -0
  89. package/build/services/pipe/functions/symbol.js +33 -0
  90. package/build/services/pipe/functions/unary.d.ts +7 -0
  91. package/build/services/pipe/functions/unary.js +18 -0
  92. package/build/services/pipe/index.d.ts +30 -0
  93. package/build/services/pipe/index.js +128 -0
  94. package/build/services/quorum/index.d.ts +34 -0
  95. package/build/services/quorum/index.js +147 -0
  96. package/build/services/reporter/index.d.ts +47 -0
  97. package/build/services/reporter/index.js +330 -0
  98. package/build/services/serializer/index.d.ts +36 -0
  99. package/build/services/serializer/index.js +222 -0
  100. package/build/services/signaler/store.d.ts +15 -0
  101. package/build/services/signaler/store.js +53 -0
  102. package/build/services/signaler/stream.d.ts +43 -0
  103. package/build/services/signaler/stream.js +317 -0
  104. package/build/services/store/cache.d.ts +66 -0
  105. package/build/services/store/cache.js +127 -0
  106. package/build/services/store/clients/ioredis.d.ts +27 -0
  107. package/build/services/store/clients/ioredis.js +96 -0
  108. package/build/services/store/clients/redis.d.ts +29 -0
  109. package/build/services/store/clients/redis.js +143 -0
  110. package/build/services/store/index.d.ts +88 -0
  111. package/build/services/store/index.js +657 -0
  112. package/build/services/stream/clients/ioredis.d.ts +23 -0
  113. package/build/services/stream/clients/ioredis.js +115 -0
  114. package/build/services/stream/clients/redis.d.ts +23 -0
  115. package/build/services/stream/clients/redis.js +119 -0
  116. package/build/services/stream/index.d.ts +21 -0
  117. package/build/services/stream/index.js +9 -0
  118. package/build/services/sub/clients/ioredis.d.ts +20 -0
  119. package/build/services/sub/clients/ioredis.js +72 -0
  120. package/build/services/sub/clients/redis.d.ts +20 -0
  121. package/build/services/sub/clients/redis.js +63 -0
  122. package/build/services/sub/index.d.ts +18 -0
  123. package/build/services/sub/index.js +9 -0
  124. package/build/services/task/index.d.ts +18 -0
  125. package/build/services/task/index.js +73 -0
  126. package/build/services/telemetry/index.d.ts +49 -0
  127. package/build/services/telemetry/index.js +223 -0
  128. package/build/services/worker/index.d.ts +30 -0
  129. package/build/services/worker/index.js +105 -0
  130. package/build/types/activity.d.ts +86 -0
  131. package/build/types/activity.js +2 -0
  132. package/build/types/app.d.ts +16 -0
  133. package/build/types/app.js +2 -0
  134. package/build/types/async.d.ts +5 -0
  135. package/build/types/async.js +2 -0
  136. package/build/types/cache.d.ts +1 -0
  137. package/build/types/cache.js +2 -0
  138. package/build/types/collator.d.ts +8 -0
  139. package/build/types/collator.js +11 -0
  140. package/build/types/durable.d.ts +59 -0
  141. package/build/types/durable.js +2 -0
  142. package/build/types/hook.d.ts +31 -0
  143. package/build/types/hook.js +9 -0
  144. package/build/types/hotmesh.d.ts +82 -0
  145. package/build/types/hotmesh.js +2 -0
  146. package/build/types/index.d.ts +20 -0
  147. package/build/types/index.js +21 -0
  148. package/build/types/ioredisclient.d.ts +5 -0
  149. package/build/types/ioredisclient.js +5 -0
  150. package/build/types/job.d.ts +50 -0
  151. package/build/types/job.js +2 -0
  152. package/build/types/logger.d.ts +6 -0
  153. package/build/types/logger.js +2 -0
  154. package/build/types/map.d.ts +4 -0
  155. package/build/types/map.js +2 -0
  156. package/build/types/pipe.d.ts +4 -0
  157. package/build/types/pipe.js +2 -0
  158. package/build/types/quorum.d.ts +46 -0
  159. package/build/types/quorum.js +2 -0
  160. package/build/types/redis.d.ts +8 -0
  161. package/build/types/redis.js +2 -0
  162. package/build/types/redisclient.d.ts +25 -0
  163. package/build/types/redisclient.js +2 -0
  164. package/build/types/serializer.d.ts +33 -0
  165. package/build/types/serializer.js +2 -0
  166. package/build/types/stats.d.ts +83 -0
  167. package/build/types/stats.js +2 -0
  168. package/build/types/stream.d.ts +67 -0
  169. package/build/types/stream.js +25 -0
  170. package/build/types/telemetry.d.ts +1 -0
  171. package/build/types/telemetry.js +11 -0
  172. package/build/types/transition.d.ts +17 -0
  173. package/build/types/transition.js +2 -0
  174. package/index.ts +5 -0
  175. package/modules/errors.ts +55 -0
  176. package/modules/key.ts +129 -0
  177. package/modules/utils.ts +170 -0
  178. package/package.json +73 -0
  179. package/services/activities/activity.ts +473 -0
  180. package/services/activities/await.ts +172 -0
  181. package/services/activities/emit.ts +25 -0
  182. package/services/activities/index.ts +15 -0
  183. package/services/activities/iterate.ts +26 -0
  184. package/services/activities/trigger.ts +196 -0
  185. package/services/activities/worker.ts +190 -0
  186. package/services/collator/README.md +102 -0
  187. package/services/collator/index.ts +182 -0
  188. package/services/compiler/deployer.ts +432 -0
  189. package/services/compiler/index.ts +98 -0
  190. package/services/compiler/validator.ts +154 -0
  191. package/services/connector/clients/ioredis.ts +57 -0
  192. package/services/connector/clients/redis.ts +72 -0
  193. package/services/connector/index.ts +44 -0
  194. package/services/dimension/README.md +73 -0
  195. package/services/dimension/index.ts +39 -0
  196. package/services/durable/asyncLocalStorage.ts +3 -0
  197. package/services/durable/client.ts +116 -0
  198. package/services/durable/connection.ts +50 -0
  199. package/services/durable/factory.ts +124 -0
  200. package/services/durable/handle.ts +43 -0
  201. package/services/durable/index.ts +60 -0
  202. package/services/durable/native.ts +46 -0
  203. package/services/durable/worker.ts +254 -0
  204. package/services/durable/workflow.ts +136 -0
  205. package/services/engine/index.ts +615 -0
  206. package/services/hotmesh/index.ts +182 -0
  207. package/services/logger/index.ts +79 -0
  208. package/services/mapper/index.ts +84 -0
  209. package/services/pipe/functions/array.ts +87 -0
  210. package/services/pipe/functions/bitwise.ts +27 -0
  211. package/services/pipe/functions/conditional.ts +31 -0
  212. package/services/pipe/functions/date.ts +214 -0
  213. package/services/pipe/functions/index.ts +25 -0
  214. package/services/pipe/functions/json.ts +11 -0
  215. package/services/pipe/functions/math.ts +143 -0
  216. package/services/pipe/functions/number.ts +150 -0
  217. package/services/pipe/functions/object.ts +79 -0
  218. package/services/pipe/functions/string.ts +86 -0
  219. package/services/pipe/functions/symbol.ts +39 -0
  220. package/services/pipe/functions/unary.ts +19 -0
  221. package/services/pipe/index.ts +138 -0
  222. package/services/quorum/index.ts +200 -0
  223. package/services/reporter/index.ts +379 -0
  224. package/services/serializer/README.md +10 -0
  225. package/services/serializer/index.ts +243 -0
  226. package/services/signaler/store.ts +61 -0
  227. package/services/signaler/stream.ts +354 -0
  228. package/services/store/cache.ts +172 -0
  229. package/services/store/clients/ioredis.ts +123 -0
  230. package/services/store/clients/redis.ts +169 -0
  231. package/services/store/index.ts +757 -0
  232. package/services/stream/clients/ioredis.ts +148 -0
  233. package/services/stream/clients/redis.ts +144 -0
  234. package/services/stream/index.ts +57 -0
  235. package/services/sub/clients/ioredis.ts +83 -0
  236. package/services/sub/clients/redis.ts +74 -0
  237. package/services/sub/index.ts +25 -0
  238. package/services/task/index.ts +86 -0
  239. package/services/telemetry/index.ts +267 -0
  240. package/services/worker/index.ts +165 -0
  241. package/types/activity.ts +115 -0
  242. package/types/app.ts +20 -0
  243. package/types/async.ts +7 -0
  244. package/types/cache.ts +1 -0
  245. package/types/collator.ts +9 -0
  246. package/types/durable.ts +81 -0
  247. package/types/hook.ts +32 -0
  248. package/types/hotmesh.ts +102 -0
  249. package/types/index.ts +138 -0
  250. package/types/ioredisclient.ts +10 -0
  251. package/types/job.ts +59 -0
  252. package/types/logger.ts +6 -0
  253. package/types/map.ts +5 -0
  254. package/types/ms.d.ts +7 -0
  255. package/types/pipe.ts +7 -0
  256. package/types/quorum.ts +59 -0
  257. package/types/redis.ts +27 -0
  258. package/types/redisclient.ts +29 -0
  259. package/types/serializer.ts +38 -0
  260. package/types/stats.ts +100 -0
  261. package/types/stream.ts +75 -0
  262. package/types/telemetry.ts +15 -0
  263. package/types/transition.ts +20 -0
@@ -0,0 +1,432 @@
1
+ import { KeyStoreParams, KeyType } from "../../modules/key";
2
+ import { getSymKey } from "../../modules/utils";
3
+ import { CollatorService } from "../collator";
4
+ import { SerializerService } from "../serializer";
5
+ import { StoreService } from '../store';
6
+ import { ActivityType } from "../../types/activity";
7
+ import { HookRule } from "../../types/hook";
8
+ import { HotMeshGraph, HotMeshManifest } from "../../types/hotmesh";
9
+ import { RedisClient, RedisMulti } from "../../types/redis";
10
+ import { StringAnyType, Symbols } from "../../types/serializer";
11
+
12
+ const DEFAULT_METADATA_RANGE_SIZE = 26; //metadata is 26 slots ([a-z] * 1)
13
+ const DEFAULT_DATA_RANGE_SIZE = 260; //data is 260 slots ([a-zA-Z] * 5)
14
+ const DEFAULT_RANGE_SIZE = DEFAULT_METADATA_RANGE_SIZE + DEFAULT_DATA_RANGE_SIZE;
15
+
16
+ class Deployer {
17
+ manifest: HotMeshManifest | null = null;
18
+ store: StoreService<RedisClient, RedisMulti> | null;
19
+
20
+ constructor(manifest: HotMeshManifest) {
21
+ this.manifest = manifest;
22
+ }
23
+
24
+ async deploy(store: StoreService<RedisClient, RedisMulti>) {
25
+ this.store = store;
26
+ CollatorService.compile(this.manifest.app.graphs);
27
+ this.convertTopicsToTypes();
28
+ this.copyJobSchemas();
29
+ this.bindBackRefs();
30
+ this.bindParents();
31
+ this.resolveMappingDependencies();
32
+ this.resolveJobMapsPaths();
33
+ await this.generateSymKeys();
34
+ await this.generateSymVals();
35
+ await this.deployHookPatterns();
36
+ await this.deployActivitySchemas();
37
+ await this.deploySubscriptions();
38
+ await this.deployTransitions();
39
+ await this.deployConsumerGroups();
40
+ }
41
+
42
+ getVID() {
43
+ return {
44
+ id: this.manifest.app.id,
45
+ version: this.manifest.app.version,
46
+ }
47
+ }
48
+
49
+ async generateSymKeys() {
50
+ //note: symbol ranges are additive (per version); path assignments are immutable
51
+ const appId = this.manifest.app.id;
52
+ for (const graph of this.manifest.app.graphs) {
53
+ //generate JOB symbols
54
+ const [,trigger] = this.findTrigger(graph);
55
+ const topic = trigger.subscribes;
56
+ const [lower, upper, symbols] = await this.store.reserveSymbolRange(`$${topic}`, DEFAULT_RANGE_SIZE, 'JOB');
57
+ const prefix = ''; //job meta/data is NOT namespaced
58
+ const newSymbols = this.bindSymbols(lower, upper, symbols, prefix, trigger.PRODUCES);
59
+ if (Object.keys(newSymbols).length) {
60
+ await this.store.addSymbols(`$${topic}`, newSymbols);
61
+ }
62
+ //generate ACTIVITY symbols
63
+ for (const [activityId, activity] of Object.entries(graph.activities)) {
64
+ const [lower, upper, symbols] = await this.store.reserveSymbolRange(activityId, DEFAULT_RANGE_SIZE, 'ACTIVITY');
65
+ const prefix = `${activityId}/`; //activity meta/data is namespaced
66
+ const newSymbols = this.bindSymbols(lower, upper, symbols, prefix, activity.produces);
67
+ if (Object.keys(newSymbols).length) {
68
+ await this.store.addSymbols(activityId, newSymbols);
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ bindSymbols(startIndex: number, maxIndex: number, existingSymbols: Symbols, prefix: string, produces: string[]): Symbols {
75
+ let newSymbols: Symbols = {};
76
+ let currentSymbols: Symbols = {...existingSymbols};
77
+ for (let path of produces) {
78
+ const fullPath = `${prefix}${path}`;
79
+ if (!currentSymbols[fullPath]) {
80
+ if (startIndex > maxIndex) {
81
+ throw new Error('Symbol index out of bounds');
82
+ }
83
+ const symbol = getSymKey(startIndex);
84
+ startIndex++
85
+ newSymbols[fullPath] = symbol;
86
+ currentSymbols[fullPath] = symbol; // update the currentSymbols to include this new symbol
87
+ }
88
+ }
89
+ return newSymbols;
90
+ }
91
+
92
+ copyJobSchemas() {
93
+ const graphs = this.manifest!.app.graphs;
94
+ for (const graph of graphs) {
95
+ const jobSchema = graph.output?.schema;
96
+ const outputSchema = graph.input?.schema;
97
+ if (!jobSchema && !outputSchema) continue;
98
+ const activities = graph.activities;
99
+ // Find the trigger activity and bind the job schema to it
100
+ // at execution time, the trigger is a standin for the job
101
+ for (const activityKey in activities) {
102
+ if (activities[activityKey].type === 'trigger') {
103
+ const trigger = activities[activityKey];
104
+ if (jobSchema) {
105
+ //possible for trigger to have job mappings
106
+ if (!trigger.job) { trigger.job = {}; }
107
+ trigger.job.schema = jobSchema;
108
+ }
109
+ if (outputSchema) {
110
+ //impossible for trigger to have output mappings.
111
+ trigger.output = { schema: outputSchema };
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ bindBackRefs() {
119
+ for (const graph of this.manifest!.app.graphs) {
120
+ const activities = graph.activities;
121
+ const triggerId = this.findTrigger(graph)[0];
122
+ for (const activityKey in activities) {
123
+ activities[activityKey].trigger = triggerId;
124
+ activities[activityKey].subscribes = graph.subscribes;
125
+ if (graph.publishes) {
126
+ activities[activityKey].publishes = graph.publishes;
127
+ }
128
+ if (graph.expire) {
129
+ activities[activityKey].expire = graph.expire;
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ //more intuitive for SDK users to use 'topic',
136
+ //but the compiler is desiged to be generic and uses 'subtypes'
137
+ convertTopicsToTypes() {
138
+ for (const graph of this.manifest!.app.graphs) {
139
+ const activities = graph.activities;
140
+ for (const activityKey in activities) {
141
+ const activity = activities[activityKey];
142
+ if (['worker', 'await'].includes(activity.type) && activity.topic && !activity.subtype) {
143
+ activity.subtype = activity.topic;
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ async bindParents() {
150
+ const graphs = this.manifest.app.graphs;
151
+ for (const graph of graphs) {
152
+ if (graph.transitions) {
153
+ for (const fromActivity in graph.transitions) {
154
+ const toTransitions = graph.transitions[fromActivity];
155
+ for (const transition of toTransitions) {
156
+ const to = transition.to;
157
+ //DAGs have one parent; easy to optimize for
158
+ graph.activities[to].parent = fromActivity;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ collectValues(schema: Record<string, any>, values: Set<string>) {
166
+ for (const [key, value] of Object.entries(schema)) {
167
+ if (key === 'enum' || key === 'examples' || key === 'default') {
168
+ if (Array.isArray(value)) {
169
+ for (const v of value) {
170
+ if (typeof v === 'string' && v.length > 5) {
171
+ values.add(v);
172
+ }
173
+ }
174
+ } else if (typeof value === 'string' && value.length > 5) {
175
+ values.add(value);
176
+ }
177
+ } else if (typeof value === 'object') {
178
+ this.collectValues(value, values);
179
+ }
180
+ }
181
+ }
182
+
183
+ traverse(obj: any, values: Set<string>) {
184
+ for (const value of Object.values(obj)) {
185
+ if (typeof value === 'object') {
186
+ if ('schema' in value) {
187
+ this.collectValues(value.schema, values);
188
+ } else {
189
+ this.traverse(value, values);
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ async generateSymVals() {
196
+ const uniqueStrings = new Set<string>();
197
+ for (const graph of this.manifest!.app.graphs) {
198
+ this.traverse(graph, uniqueStrings);
199
+ }
200
+ const existingSymbols = await this.store.getSymbolValues();
201
+ const startIndex = Object.keys(existingSymbols).length;
202
+ const maxIndex = Math.pow(52, 2) - 1;
203
+ const newSymbols = SerializerService.filterSymVals(startIndex, maxIndex, existingSymbols, uniqueStrings);
204
+ await this.store.addSymbolValues(newSymbols);
205
+ }
206
+
207
+ resolveJobMapsPaths() {
208
+ function parsePaths(obj: StringAnyType): string[] {
209
+ let result = [];
210
+ function traverse(obj: StringAnyType, path = []) {
211
+ for (let key in obj) {
212
+ if (typeof obj[key] === 'object' && obj[key] !== null && !('@pipe' in obj[key])) {
213
+ let newPath = [...path, key];
214
+ traverse(obj[key], newPath);
215
+ } else {
216
+ const finalPath = `data/${[...path, key].join('/')}`;
217
+ if (!result.includes(finalPath)) {
218
+ result.push(finalPath);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ if (obj) {
224
+ traverse(obj);
225
+ }
226
+ return result;
227
+ }
228
+
229
+ for (const graph of this.manifest.app.graphs) {
230
+ let results: string[] = [];
231
+ const [, trigger] = this.findTrigger(graph);
232
+ for (const activityKey in graph.activities) {
233
+ const activity = graph.activities[activityKey];
234
+ results = results.concat(parsePaths(activity.job?.maps));
235
+ }
236
+ trigger.PRODUCES = results;
237
+ }
238
+ }
239
+
240
+ resolveMappingDependencies() {
241
+ const dynamicMappingRules: string[] = [];
242
+ //recursive function to descend into the object and find all dynamic mapping rules
243
+ function traverse(obj: StringAnyType, consumes: string[]): void {
244
+ for (const key in obj) {
245
+ if (typeof obj[key] === 'string') {
246
+ const stringValue = obj[key] as string;
247
+ const dynamicMappingRuleMatch = stringValue.match(/^\{[^@].*}$/);
248
+ if (dynamicMappingRuleMatch) {
249
+ if (stringValue.split('.')[1] !== 'input') {
250
+ dynamicMappingRules.push(stringValue);
251
+ consumes.push(stringValue);
252
+ }
253
+ }
254
+ } else if (typeof obj[key] === 'object' && obj[key] !== null) {
255
+ traverse(obj[key], consumes);
256
+ }
257
+ }
258
+ }
259
+ const graphs = this.manifest.app.graphs;
260
+ for (const graph of graphs) {
261
+ const activities = graph.activities;
262
+ for (const activityId in activities) {
263
+ const activity = activities[activityId];
264
+ activity.consumes = [];
265
+ traverse(activity, activity.consumes);
266
+ activity.consumes = this.groupMappingRules(activity.consumes);
267
+ }
268
+ }
269
+ const groupedRules = this.groupMappingRules(dynamicMappingRules);
270
+ // Iterate through the graph and add 'produces' field to each activity
271
+ for (const graph of graphs) {
272
+ const activities = graph.activities;
273
+ for (const activityId in activities) {
274
+ const activity = activities[activityId];
275
+ activity.produces = groupedRules[`${activityId}`] || [];
276
+ }
277
+ }
278
+ }
279
+
280
+ groupMappingRules(rules: string[]): Record<string, string[]> {
281
+ rules = Array.from(new Set(rules)).sort();
282
+ // Group by the first symbol before the period (this is the activity name)
283
+ const groupedRules: { [key: string]: string[] } = {};
284
+ for (const rule of rules) {
285
+ const [group, resolved] = this.resolveMappableValue(rule);
286
+ if (!groupedRules[group]) {
287
+ groupedRules[group] = [];
288
+ }
289
+ groupedRules[group].push(resolved);
290
+ }
291
+ return groupedRules;
292
+ }
293
+
294
+ resolveMappableValue(mappable: string): [string, string] {
295
+ mappable = mappable.substring(1, mappable.length - 1);
296
+ const parts = mappable.split('.');
297
+ if (parts[0] === '$job') {
298
+ const [group, ...path] = parts;
299
+ return [group, path.join('/')];
300
+ } else {
301
+ //normalize paths to be relative to the activity
302
+ const [group, type, subtype, ...path] = parts;
303
+ const prefix = {
304
+ hook: 'hook/data',
305
+ input: 'input/data',
306
+ output: subtype === 'data' ? 'output/data': 'output/metadata'
307
+ }[type];
308
+ return [group, `${prefix}/${path.join('/')}`];
309
+ }
310
+ }
311
+
312
+ async deployActivitySchemas() {
313
+ const graphs = this.manifest!.app.graphs;
314
+ const activitySchemas: Record<string, ActivityType> = {};
315
+ for (const graph of graphs) {
316
+ const activities = graph.activities;
317
+ for (const activityKey in activities) {
318
+ activitySchemas[activityKey] = activities[activityKey];
319
+ }
320
+ }
321
+ await this.store.setSchemas(activitySchemas, this.getVID());
322
+ }
323
+
324
+ async deploySubscriptions() {
325
+ const graphs = this.manifest!.app.graphs;
326
+ const publicSubscriptions: { [key: string]: string } = {};
327
+ for (const graph of graphs) {
328
+ const activities = graph.activities;
329
+ const subscribesTopic = graph.subscribes;
330
+ // Find the activity ID associated with the subscribes topic
331
+ for (const activityKey in activities) {
332
+ if (activities[activityKey].type === 'trigger') {
333
+ publicSubscriptions[subscribesTopic] = activityKey;
334
+ break;
335
+ }
336
+ }
337
+ }
338
+ await this.store.setSubscriptions(publicSubscriptions, this.getVID());
339
+ }
340
+
341
+ findTrigger(graph: HotMeshGraph): [string, Record<string, any>] | null {
342
+ for (const activityKey in graph.activities) {
343
+ const activity = graph.activities[activityKey];
344
+ if (activity.type === 'trigger') {
345
+ return [activityKey, activity];
346
+ }
347
+ }
348
+ return null;
349
+ }
350
+
351
+ async deployTransitions() {
352
+ const graphs = this.manifest!.app.graphs;
353
+ const privateSubscriptions: { [key: string]: any } = {};
354
+ for (const graph of graphs) {
355
+ if (graph.subscribes && graph.subscribes.startsWith('.')) {
356
+ const [triggerId] = this.findTrigger(graph);
357
+ if (triggerId) {
358
+ privateSubscriptions[graph.subscribes] = { [triggerId]: true };
359
+ }
360
+ }
361
+ if (graph.transitions) {
362
+ for (const fromActivity in graph.transitions) {
363
+ const toTransitions = graph.transitions[fromActivity];
364
+ const toValues: { [key: string]: any } = {};
365
+ for (const transition of toTransitions) {
366
+ const to = transition.to;
367
+ if (transition.conditions) {
368
+ toValues[to] = transition.conditions;
369
+ } else {
370
+ toValues[to] = true;
371
+ }
372
+ }
373
+ if (Object.keys(toValues).length > 0) {
374
+ privateSubscriptions['.' + fromActivity] = toValues;
375
+ }
376
+ }
377
+ }
378
+ }
379
+ await this.store.setTransitions(privateSubscriptions, this.getVID());
380
+ }
381
+
382
+ async deployHookPatterns() {
383
+ const graphs = this.manifest!.app.graphs;
384
+ const hookRules: Record<string, HookRule[]> = {};
385
+ for (const graph of graphs) {
386
+ if (graph.hooks) {
387
+ for (const topic in graph.hooks) {
388
+ hookRules[topic] = graph.hooks[topic];
389
+ const activityId = graph.hooks[topic][0].to;
390
+ const targetActivity = graph.activities[activityId];
391
+ if (targetActivity) {
392
+ if (!targetActivity.hook) {
393
+ targetActivity.hook = {};
394
+ }
395
+ //create back-reference to the hook topic
396
+ targetActivity.hook.topic = topic;
397
+ }
398
+ }
399
+ }
400
+ }
401
+ await this.store.setHookRules(hookRules);
402
+ }
403
+
404
+ async deployConsumerGroups() {
405
+ //create one engine group
406
+ const params: KeyStoreParams = { appId: this.manifest.app.id }
407
+ const key = this.store.mintKey(KeyType.STREAMS, params);
408
+ await this.deployConsumerGroup(key, 'ENGINE');
409
+ for (const graph of this.manifest.app.graphs) {
410
+ const activities = graph.activities;
411
+ for (const activityKey in activities) {
412
+ const activity = activities[activityKey];
413
+ if (activity.type === 'worker') {
414
+ params.topic = activity.subtype;
415
+ const key = this.store.mintKey(KeyType.STREAMS, params);
416
+ //create one worker group per unique activity subtype (the topic)
417
+ await this.deployConsumerGroup(key, 'WORKER');
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ async deployConsumerGroup(stream: string, group: string) {
424
+ try {
425
+ await this.store.xgroup('CREATE', stream, group, '$', 'MKSTREAM');
426
+ } catch (err) {
427
+ this.store.logger.info('consumer-group-exists', { stream, group });
428
+ }
429
+ }
430
+ }
431
+
432
+ export { Deployer };
@@ -0,0 +1,98 @@
1
+ import $RefParser from '@apidevtools/json-schema-ref-parser';
2
+ import yaml from 'js-yaml';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+
6
+ import { ILogger } from '../logger';
7
+ import { StoreService } from '../store';
8
+ import { Deployer } from './deployer';
9
+ import { Validator } from './validator';
10
+ import { HotMeshManifest } from '../../types/hotmesh';
11
+ import { RedisClient, RedisMulti } from '../../types/redis';
12
+
13
+ /**
14
+ * The compiler service converts a graph into a executable program.
15
+ */
16
+ class CompilerService {
17
+ store: StoreService<RedisClient, RedisMulti> | null;
18
+ logger: ILogger;
19
+
20
+ constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
21
+ this.store = store;
22
+ this.logger = logger;
23
+ }
24
+
25
+ /**
26
+ * verifies and plans the deployment of an app to Redis; the app is not deployed yet
27
+ * @param path
28
+ */
29
+ async plan(mySchemaOrPath: string): Promise<HotMeshManifest> {
30
+ try {
31
+ let schema: HotMeshManifest;
32
+ if (this.isPath(mySchemaOrPath)) {
33
+ schema = await $RefParser.dereference(mySchemaOrPath) as HotMeshManifest;
34
+ } else {
35
+ schema = yaml.load(mySchemaOrPath) as HotMeshManifest;
36
+ }
37
+
38
+ // 1) validate the manifest file
39
+ const validator = new Validator(schema);
40
+ validator.validate(this.store);
41
+
42
+ // 2) todo: add a PlannerService module that will plan the deployment (what might break, drift, etc)
43
+ return schema as HotMeshManifest
44
+ } catch(err) {
45
+ this.logger.error('compiler-plan-error', err);
46
+ }
47
+ }
48
+
49
+ isPath(input: string): boolean {
50
+ return !input.trim().startsWith('app:');
51
+ }
52
+
53
+ /**
54
+ * deploys an app to Redis but does NOT activate it.
55
+ */
56
+ async deploy(mySchemaOrPath: string): Promise<HotMeshManifest> {
57
+ try {
58
+ let schema: HotMeshManifest;
59
+ if (this.isPath(mySchemaOrPath)) {
60
+ schema = await $RefParser.dereference(mySchemaOrPath) as HotMeshManifest;
61
+ await this.saveAsJSON(mySchemaOrPath, schema);
62
+ } else {
63
+ schema = yaml.load(mySchemaOrPath) as HotMeshManifest;
64
+ }
65
+
66
+ // 2) validate the manifest file (synchronous operation...no callbacks)
67
+ const validator = new Validator(schema);
68
+ validator.validate(this.store);
69
+
70
+ // 3) deploy the schema (segment, optimize, etc; save to Redis)
71
+ const deployer = new Deployer(schema);
72
+ await deployer.deploy(this.store);
73
+
74
+ // 4) save the app version to Redis (so it can be activated later)
75
+ await this.store.setApp(schema.app.id, schema.app.version);
76
+ return schema;
77
+ } catch(err) {
78
+ this.logger.error('compiler-deploy-error', err);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * activates a deployed version of an app;
84
+ * @param appId
85
+ * @param appVersion
86
+ */
87
+ async activate(appId: string, appVersion: string): Promise<boolean> {
88
+ return await this.store.activateAppVersion(appId, appVersion);
89
+ }
90
+
91
+ async saveAsJSON(originalPath: string, schema: HotMeshManifest): Promise<void> {
92
+ const json = JSON.stringify(schema, null, 2);
93
+ const newPath = path.join( path.dirname(originalPath), `.hotmesh.${schema.app.id}.${schema.app.version}.json` );
94
+ await fs.writeFile(newPath, json, 'utf8');
95
+ }
96
+ }
97
+
98
+ export { CompilerService };
@@ -0,0 +1,154 @@
1
+ import { Pipe } from "../pipe";
2
+ import { StoreService } from '../store';
3
+ import { MappingStatements } from "../../types/map";
4
+ import { HotMeshManifest } from "../../types/hotmesh";
5
+ import { RedisClient, RedisMulti } from "../../types/redis";
6
+
7
+ class Validator {
8
+ manifest: HotMeshManifest | null = null;
9
+ activityIds: string[] = [];
10
+ mappingStatements: MappingStatements = {};
11
+ store: StoreService<RedisClient, RedisMulti> | null = null;
12
+
13
+ static SYS_VARS = ['$app', '$self', '$graph', '$job'];
14
+
15
+ constructor(manifest: HotMeshManifest) {
16
+ this.manifest = manifest;
17
+ }
18
+
19
+ /**
20
+ * validate the manifest file
21
+ */
22
+ async validate(store: StoreService<RedisClient, RedisMulti>) {
23
+ this.store = store;
24
+ this.getMappingStatements();
25
+ this.validateActivityIds();
26
+ this.validateReferencedActivityIds();
27
+ this.validateMappingStatements();
28
+ this.validateTransitions();
29
+ this.validateTransitionConditions();
30
+ this.validateStats();
31
+ this.validateSchemas();
32
+ this.validateUniqueHandledTopics();
33
+ this.validateGraphPublishSubscribe();
34
+ this.validateHooks();
35
+ this.validateConditionalStatements();
36
+ }
37
+
38
+ // 1.1) Validate the manifest file activity ids are unique (no duplicates)
39
+ validateActivityIds() {
40
+ const activityIdsSet: Set<string> = new Set();
41
+ this.manifest.app.graphs.forEach((graph) => {
42
+ const ids = Object.keys(graph.activities);
43
+ // Check for duplicates and add ids to the set
44
+ ids.forEach((id) => {
45
+ if (activityIdsSet.has(id)) {
46
+ throw new Error(`Duplicate activity id found: ${id}`);
47
+ } else {
48
+ activityIdsSet.add(id);
49
+ }
50
+ });
51
+ });
52
+ this.activityIds = Array.from(activityIdsSet);
53
+ }
54
+
55
+ isMappingStatement(value: string): boolean {
56
+ return typeof value === 'string' && value.startsWith('{') && value.endsWith('}');
57
+ }
58
+
59
+ extractMappingStatements(obj: any, result: MappingStatements, currentActivityId: string): void {
60
+ for (const key in obj) {
61
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
62
+ this.extractMappingStatements(obj[key], result, currentActivityId);
63
+ } else if (this.isMappingStatement(obj[key])) {
64
+ if (!result[currentActivityId]) {
65
+ result[currentActivityId] = [];
66
+ }
67
+ result[currentActivityId].push(obj[key]);
68
+ }
69
+ }
70
+ }
71
+
72
+ getMappingStatements() {
73
+ const mappingStatements: MappingStatements = {};
74
+ this.manifest.app.graphs.forEach((graph) => {
75
+ const activities = graph.activities;
76
+ for (const activityId in activities) {
77
+ const activity = activities[activityId];
78
+ this.extractMappingStatements(activity, mappingStatements, activityId);
79
+ }
80
+ });
81
+ this.mappingStatements = mappingStatements;
82
+ }
83
+
84
+ // 1.2) Validate no activity ids are referenced that don't exist
85
+ validateReferencedActivityIds() {
86
+ // get list of all mapping statements and validate
87
+ const mappingStatements = this.mappingStatements;
88
+ const activityIds = this.activityIds;
89
+ for (const activity in mappingStatements) {
90
+ const statements = mappingStatements[activity];
91
+ statements.forEach((statement) => {
92
+ if (statement.startsWith('{') && statement.endsWith('}')) {
93
+ const statementParts = statement.slice(1, -1).split('.');
94
+ const referencedActivityId = statementParts[0];
95
+
96
+ if (!(Validator.SYS_VARS.includes(referencedActivityId) || activityIds.includes(referencedActivityId) || this.isFunction(statement))) {
97
+ throw new Error(`Mapping statement references non-existent activity: ${statement}`);
98
+ }
99
+ }
100
+ });
101
+ }
102
+ }
103
+
104
+ isFunction(value: string): boolean {
105
+ return value.startsWith('{@') && Pipe.resolveFunction(value);
106
+ }
107
+
108
+ // 1.3) Validate the mapping/@pipe statements are valid
109
+ validateMappingStatements() {
110
+ // Implement the method content
111
+ }
112
+
113
+ // 1.4) Validate the transitions are valid
114
+ validateTransitions() {
115
+ // Implement the method content
116
+ }
117
+
118
+ // 1.5) Validate the transition conditions are valid
119
+ validateTransitionConditions() {
120
+ // Implement the method content
121
+ }
122
+
123
+ // 1.6) Validate the stats
124
+ validateStats() {
125
+ // Implement the method content
126
+ }
127
+
128
+ // 1.7) Validate the schemas
129
+ validateSchemas() {
130
+ // Implement the method content
131
+ }
132
+
133
+ // 1.8) Validate the topics are unique and handled
134
+ validateUniqueHandledTopics() {
135
+ // Implement the method content
136
+ }
137
+
138
+ // 1.9) Validate that every graph has publishes and subscribes
139
+ validateGraphPublishSubscribe() {
140
+ // Implement the method content
141
+ }
142
+
143
+ // 1.10) Validate hooks, including mapping statements
144
+ validateHooks() {
145
+ // Implement the method content
146
+ }
147
+
148
+ // 1.11) Validate conditional statements
149
+ validateConditionalStatements() {
150
+ // Implement the method content
151
+ }
152
+ }
153
+
154
+ export { Validator };