@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,200 @@
1
+ import { KeyType } from '../../modules/key';
2
+ import { identifyRedisType, sleepFor } from '../../modules/utils';
3
+ import { CompilerService } from '../compiler';
4
+ import { EngineService } from '../engine';
5
+ import { ILogger } from '../logger';
6
+ import { StoreService } from '../store';
7
+ import { RedisStoreService as RedisStore } from '../store/clients/redis';
8
+ import { IORedisStoreService as IORedisStore } from '../store/clients/ioredis';
9
+ import { SubService } from '../sub';
10
+ import { IORedisSubService as IORedisSub } from '../sub/clients/ioredis';
11
+ import { RedisSubService as RedisSub } from '../sub/clients/redis';
12
+ import { CacheMode } from '../../types/cache';
13
+ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient';
14
+ import {
15
+ QuorumMessage,
16
+ QuorumMessageCallback,
17
+ SubscriptionCallback,
18
+ ThrottleMessage
19
+ } from '../../types/quorum';
20
+ import { HotMeshApps, HotMeshConfig } from '../../types/hotmesh';
21
+ import { RedisClient, RedisMulti } from '../../types/redis';
22
+ import { RedisClientType } from '../../types/redisclient';
23
+
24
+ //wait time to see if quorum is reached
25
+ const QUORUM_DELAY = 250;
26
+
27
+ class QuorumService {
28
+ namespace: string;
29
+ apps: HotMeshApps | null;
30
+ appId: string;
31
+ guid: string;
32
+ engine: EngineService;
33
+ store: StoreService<RedisClient, RedisMulti> | null;
34
+ subscribe: SubService<RedisClient, RedisMulti> | null;
35
+ logger: ILogger;
36
+ cacheMode: CacheMode = 'cache';
37
+ untilVersion: string | null = null;
38
+ quorum: number | null = null;
39
+ callbacks: QuorumMessageCallback[] = [];
40
+
41
+ static async init(
42
+ namespace: string,
43
+ appId: string,
44
+ guid: string,
45
+ config: HotMeshConfig,
46
+ engine: EngineService,
47
+ logger: ILogger
48
+ ): Promise<QuorumService> {
49
+ if (config.engine) {
50
+ const instance = new QuorumService();
51
+ instance.verifyQuorumFields(config);
52
+ instance.namespace = namespace;
53
+ instance.appId = appId;
54
+ instance.guid = guid;
55
+ instance.logger = logger;
56
+ instance.engine = engine;
57
+
58
+ //note: `quorum` shares/re-uses the engine's `store`/`sub` Redis clients
59
+ await instance.initStoreChannel(config.engine.store);
60
+ await instance.initSubChannel(config.engine.sub);
61
+ await instance.subscribe.subscribe(KeyType.QUORUM, instance.subscriptionHandler(), appId); //general quorum subscription
62
+ await instance.subscribe.subscribe(KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid); //app-specific quorum subscription (used for pubsub one-time request/response)
63
+
64
+ instance.engine.processWebHooks();
65
+ instance.engine.processTimeHooks();
66
+ return instance;
67
+ }
68
+ }
69
+
70
+ verifyQuorumFields(config: HotMeshConfig) {
71
+ if (!identifyRedisType(config.engine.store) ||
72
+ !identifyRedisType(config.engine.sub)) {
73
+ throw new Error('quorum config must include `store` and `sub` fields.');
74
+ }
75
+ }
76
+
77
+ async initStoreChannel(store: RedisClient) {
78
+ if (identifyRedisType(store) === 'redis') {
79
+ this.store = new RedisStore(store as RedisClientType);
80
+ } else {
81
+ this.store = new IORedisStore(store as IORedisClientType);
82
+ }
83
+ await this.store.init(
84
+ this.namespace,
85
+ this.appId,
86
+ this.logger
87
+ );
88
+ }
89
+
90
+ async initSubChannel(sub: RedisClient) {
91
+ if (identifyRedisType(sub) === 'redis') {
92
+ this.subscribe = new RedisSub(sub as RedisClientType);
93
+ } else {
94
+ this.subscribe = new IORedisSub(sub as IORedisClientType);
95
+ }
96
+ await this.subscribe.init(
97
+ this.namespace,
98
+ this.appId,
99
+ this.guid,
100
+ this.logger
101
+ );
102
+ }
103
+
104
+ subscriptionHandler(): SubscriptionCallback {
105
+ const self = this;
106
+ return async (topic: string, message: QuorumMessage) => {
107
+ self.logger.debug('quorum-event-received', { topic, type: message.type});
108
+ if (message.type === 'activate') {
109
+ self.engine.setCacheMode(message.cache_mode, message.until_version);
110
+ } else if (message.type === 'ping') {
111
+ this.sayPong(self.appId, self.guid, message.originator);
112
+ } else if (message.type === 'pong' && self.guid === message.originator) {
113
+ self.quorum = self.quorum + 1;
114
+ } else if (message.type === 'throttle') {
115
+ self.engine.throttle(message.throttle);
116
+ } else if (message.type === 'work') {
117
+ self.engine.processWebHooks()
118
+ } else if (message.type === 'job') {
119
+ self.engine.routeToSubscribers(message.topic, message.job)
120
+ }
121
+ //if there are any callbacks, call them
122
+ if (self.callbacks.length > 0) {
123
+ self.callbacks.forEach(cb => cb(topic, message));
124
+ }
125
+ };
126
+ }
127
+
128
+ async sayPong(appId: string, guid: string, originator: string) {
129
+ this.store.publish(
130
+ KeyType.QUORUM,
131
+ { type: 'pong', guid, originator },
132
+ appId,
133
+ );
134
+ }
135
+
136
+ async requestQuorum(delay = QUORUM_DELAY): Promise<number> {
137
+ const quorum = this.quorum;
138
+ this.quorum = 0;
139
+ await this.store.publish(
140
+ KeyType.QUORUM,
141
+ { type: 'ping', originator: this.guid },
142
+ this.appId,
143
+ );
144
+ await sleepFor(delay);
145
+ return quorum;
146
+ }
147
+
148
+
149
+ // ************* PUB/SUB METHODS *************
150
+ //publish a message to the quorum
151
+ async pub(quorumMessage: ThrottleMessage) {
152
+ return await this.store.publish(KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
153
+ }
154
+ //subscribe user to quorum messages
155
+ async sub(callback: QuorumMessageCallback): Promise<void> {
156
+ //the quorum is always subscribed to the `quorum` topic; just register the fn
157
+ this.callbacks.push(callback);
158
+ }
159
+ //unsubscribe user from quorum messages
160
+ async unsub(callback: QuorumMessageCallback): Promise<void> {
161
+ //the quorum is always subscribed to the `quorum` topic; just unregister the fn
162
+ this.callbacks = this.callbacks.filter(cb => cb !== callback);
163
+ }
164
+
165
+
166
+ // ************* COMPILER METHODS *************
167
+ async activate(version: string, delay = QUORUM_DELAY): Promise<boolean> {
168
+ version = version.toString();
169
+ const config = await this.engine.getVID();
170
+ //request a quorum to activate the version
171
+ await this.requestQuorum(delay);
172
+ const q1 = await this.requestQuorum(delay);
173
+ const q2 = await this.requestQuorum(delay);
174
+ const q3 = await this.requestQuorum(delay);
175
+ if (q1 && q1 === q2 && q2 === q3) {
176
+ this.logger.info('quorum-rollcall-succeeded', { q1, q2, q3 });
177
+ this.store.publish(
178
+ KeyType.QUORUM,
179
+ { type: 'activate', cache_mode: 'nocache', until_version: version },
180
+ this.appId
181
+ );
182
+ await new Promise(resolve => setTimeout(resolve, delay));
183
+ //confirm we received the activation message
184
+ if (this.engine.untilVersion === version) {
185
+ this.logger.info('quorum-activation-succeeded', { version });
186
+ const { id } = config;
187
+ const compiler = new CompilerService(this.store, this.logger);
188
+ return await compiler.activate(id, version);
189
+ } else {
190
+ this.logger.error('quorum-activation-error', { version });
191
+ throw new Error(`UntilVersion Not Received. Version ${version} not activated`);
192
+ }
193
+ } else {
194
+ this.logger.info('quorum-rollcall-error', { q1, q2, q3 });
195
+ throw new Error(`Quorum not reached. Version ${version} not activated.`);
196
+ }
197
+ }
198
+ }
199
+
200
+ export { QuorumService }
@@ -0,0 +1,379 @@
1
+ import { ILogger } from '../logger';
2
+ import { Pipe } from '../pipe';
3
+ import { StoreService } from '../store';
4
+ import { TriggerActivity } from '../../types/activity';
5
+ import { AppVID } from '../../types/app';
6
+ import { JobState } from '../../types/job';
7
+ import { RedisClient, RedisMulti } from '../../types/redis';
8
+ import {
9
+ GetStatsOptions,
10
+ StatsResponse,
11
+ AggregatedData,
12
+ Measure,
13
+ Segment,
14
+ JobStatsRange,
15
+ IdsData,
16
+ IdsResponse,
17
+ MeasureIds,
18
+ TimeSegment,
19
+ CountByFacet,
20
+ StatsType,
21
+ StatType } from '../../types/stats';
22
+
23
+ class ReporterService {
24
+ private appVersion: AppVID;
25
+ private logger: ILogger;
26
+ private store: StoreService<RedisClient, RedisMulti>;
27
+ static DEFAULT_GRANULARITY = '5m';
28
+
29
+ constructor(appVersion: AppVID, store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
30
+ this.appVersion = appVersion;
31
+ this.logger = logger;
32
+ this.store = store;
33
+ }
34
+
35
+ async getStats(options: GetStatsOptions): Promise<StatsResponse> {
36
+ this.logger.debug('reporter-getstats-started', options);
37
+ const { key, granularity, range, end, start } = options;
38
+ this.validateOptions(options);
39
+ const dateTimeSets = this.generateDateTimeSets(granularity, range, end, start);
40
+ const redisKeys = dateTimeSets.map((dateTime) => this.buildRedisKey(key, dateTime));
41
+ const rawData = await this.store.getJobStats(redisKeys);
42
+ const [count, aggregatedData] = this.aggregateData(rawData);
43
+ const statsResponse = this.buildStatsResponse(rawData, redisKeys, aggregatedData, count, options);
44
+ return statsResponse;
45
+ }
46
+
47
+ private validateOptions(options: GetStatsOptions): void {
48
+ const { start, end, range } = options;
49
+ if (start && end && range || !start && !end && !range) {
50
+ throw new Error('Invalid combination of start, end, and range values. Provide either start+end, end+range, or start+range.');
51
+ }
52
+ }
53
+ private generateDateTimeSets(granularity: string, range: string|undefined, end: string, start?: string): string[] {
54
+ if (!range) {
55
+ //pluck just a single value when no range provided
56
+ range = '0m';
57
+ }
58
+ const granularitiesInMinutes = {
59
+ '5m': 5,
60
+ '10m': 10,
61
+ '15m': 15,
62
+ '30m': 30,
63
+ '1h': 60,
64
+ };
65
+ const granularityMinutes = granularitiesInMinutes[granularity];
66
+ if (!granularityMinutes) {
67
+ throw new Error('Invalid granularity value.');
68
+ }
69
+ const rangeMinutes = this.convertRangeToMinutes(range);
70
+ if (rangeMinutes === null) {
71
+ throw new Error('Invalid range value.');
72
+ }
73
+ // If start is provided, use it. Otherwise, calculate it from the end time and range.
74
+ let startTime;
75
+ let endTime;
76
+ if (start) {
77
+ startTime = new Date(start);
78
+ endTime = new Date(startTime.getTime() + rangeMinutes * 60 * 1000);
79
+ } else {
80
+ endTime = end === 'NOW' ? new Date() : new Date(end);
81
+ startTime = new Date(endTime.getTime() - rangeMinutes * 60 * 1000);
82
+ }
83
+ // Round the start time to the nearest granularity unit
84
+ startTime.setUTCMinutes(
85
+ Math.floor(startTime.getUTCMinutes() / granularityMinutes) * granularityMinutes
86
+ );
87
+ const dateTimeSets: string[] = [];
88
+ for (
89
+ let time = startTime;
90
+ time <= endTime;
91
+ time.setUTCMinutes(time.getUTCMinutes() + granularityMinutes)
92
+ ) {
93
+ const formattedTime = [
94
+ time.getUTCFullYear(),
95
+ String(time.getUTCMonth() + 1).padStart(2, '0'),
96
+ String(time.getUTCDate()).padStart(2, '0'),
97
+ String(time.getUTCHours()).padStart(2, '0'),
98
+ String(time.getUTCMinutes()).padStart(2, '0'),
99
+ ].join('');
100
+ dateTimeSets.push(formattedTime);
101
+ }
102
+ return dateTimeSets;
103
+ }
104
+
105
+ private convertRangeToMinutes(range: string): number | null {
106
+ const timeUnit = range.slice(-1);
107
+ const value = parseInt(range.slice(0, -1), 10);
108
+ if (isNaN(value)) {
109
+ return null;
110
+ }
111
+ switch (timeUnit) {
112
+ case 'm':
113
+ return value;
114
+ case 'h':
115
+ return value * 60;
116
+ case 'd':
117
+ return value * 60 * 24;
118
+ default:
119
+ return null;
120
+ }
121
+ }
122
+
123
+ private buildRedisKey(key: string, dateTime: string, subTarget = ''): string {
124
+ return `hmsh:${this.appVersion.id}:s:${key}:${dateTime}${subTarget?':'+subTarget:''}`;
125
+ }
126
+
127
+ private aggregateData(rawData: JobStatsRange): [number, AggregatedData] {
128
+ const aggregatedData: AggregatedData = {};
129
+ let count = 0;
130
+ Object.entries(rawData).forEach(([_, data]) => {
131
+ for (const key in data) {
132
+ if (key.startsWith('count:')) {
133
+ const target = key.slice('count:'.length);
134
+ if (!aggregatedData[target]) {
135
+ aggregatedData[target] = 0;
136
+ }
137
+ aggregatedData[target] += data[key];
138
+ } else if (key === 'count') {
139
+ count += data[key];
140
+ }
141
+ }
142
+ });
143
+ return [count, aggregatedData];
144
+ }
145
+
146
+ private buildStatsResponse(rawData: JobStatsRange, redisKeys: string[], aggregatedData: AggregatedData, count: number, options: GetStatsOptions): StatsResponse {
147
+ const measures: Measure[] = [];
148
+ const measureKeys = Object.keys(aggregatedData).filter((key) => key !== "count");
149
+ let segments = undefined;
150
+ if (options.sparse !== true) {
151
+ segments = this.handleSegments(rawData, redisKeys);
152
+ }
153
+ measureKeys.forEach((key) => {
154
+ const measure: Measure = {
155
+ target: key,
156
+ type: "count",
157
+ value: aggregatedData[key],
158
+ };
159
+ measures.push(measure);
160
+ });
161
+ const response: StatsResponse = {
162
+ key: options.key,
163
+ granularity: options.granularity,
164
+ range: options.range,
165
+ end: options.end,
166
+ count,
167
+ measures: measures,
168
+ };
169
+ if (segments) {
170
+ response.segments = segments;
171
+ }
172
+ return response;
173
+ }
174
+
175
+ private handleSegments(data: JobStatsRange, hashKeys: string[]): Segment[] {
176
+ const segments: Segment[] = [];
177
+ hashKeys.forEach((hashKey, index) => {
178
+ const segmentData: Measure[] = [];
179
+ data[hashKey] && Object.entries(data[hashKey]).forEach(([key, value]) => {
180
+ if (key.startsWith('count:')) {
181
+ const target = key.slice('count:'.length);
182
+ segmentData.push({ target, type: 'count', value });
183
+ }
184
+ });
185
+ const isoTimestamp = this.isoTimestampFromKeyTimestamp(hashKey);
186
+ const count = data[hashKey] ? data[hashKey].count : 0;
187
+ segments.push({ count, time: isoTimestamp, measures: segmentData });
188
+ });
189
+ return segments;
190
+ }
191
+
192
+ private isoTimestampFromKeyTimestamp(hashKey: string): string {
193
+ const keyTimestamp = hashKey.slice(-12);
194
+ const year = keyTimestamp.slice(0, 4);
195
+ const month = keyTimestamp.slice(4, 6);
196
+ const day = keyTimestamp.slice(6, 8);
197
+ const hour = keyTimestamp.slice(8, 10);
198
+ const minute = keyTimestamp.slice(10, 12);
199
+ return `${year}-${month}-${day}T${hour}:${minute}Z`;
200
+ }
201
+
202
+ async getIds(options: GetStatsOptions, facets: string[], idRange: [number, number] = [0, -1]): Promise<IdsResponse> {
203
+ if (!facets.length) {
204
+ const stats = await this.getStats(options);
205
+ facets = this.getUniqueFacets(stats);
206
+ }
207
+ const { key, granularity, range, end, start } = options;
208
+ this.validateOptions(options);
209
+ let redisKeys: string[] = [];
210
+ facets.forEach((facet) => {
211
+ const dateTimeSets = this.generateDateTimeSets(granularity, range, end, start);
212
+ redisKeys = redisKeys.concat(dateTimeSets.map((dateTime) => this.buildRedisKey(key, dateTime, `index:${facet}`)));
213
+ });
214
+ const idsData = await this.store.getJobIds(redisKeys, idRange);
215
+ const idsResponse = this.buildIdsResponse(idsData, options, facets);
216
+ return idsResponse;
217
+ }
218
+
219
+ private buildIdsResponse(idsData: IdsData, options: GetStatsOptions, facets: string[]): IdsResponse {
220
+ const countsByFacet: { [key: string]: number } = {};
221
+ const measureKeys = Object.keys(idsData);
222
+ measureKeys.forEach((key) => {
223
+ const target = this.getTargetForKey(key as string);
224
+ const count = idsData[key].length;
225
+
226
+ if (countsByFacet[target]) {
227
+ countsByFacet[target] += count;
228
+ } else {
229
+ countsByFacet[target] = count;
230
+ }
231
+ });
232
+ const counts: CountByFacet[] = Object.entries(countsByFacet).map(([facet, count]) => ({ facet, count }));
233
+ const response: IdsResponse = {
234
+ key: options.key,
235
+ facets,
236
+ granularity: options.granularity,
237
+ range: options.range,
238
+ start: options.start,
239
+ counts,
240
+ segments: this.buildTimeSegments(idsData),
241
+ };
242
+ return response;
243
+ }
244
+
245
+ private buildTimeSegments(idsData: IdsData): TimeSegment[] {
246
+ const measureKeys = Object.keys(idsData);
247
+ const timeSegments: { [time: string]: MeasureIds[] } = {};
248
+
249
+ measureKeys.forEach((key) => {
250
+ const measure: MeasureIds = {
251
+ type: 'ids',
252
+ target: this.getTargetForKey(key as string),
253
+ time: this.isoTimestampFromKeyTimestamp(this.getTargetForTime(key as string)),
254
+ count: idsData[key].length,
255
+ ids: idsData[key],
256
+ };
257
+
258
+ if (timeSegments[measure.time]) {
259
+ timeSegments[measure.time].push(measure);
260
+ } else {
261
+ timeSegments[measure.time] = [measure];
262
+ }
263
+ });
264
+
265
+ const segments: TimeSegment[] = Object.entries(timeSegments).map(([time, measures]) => ({
266
+ time,
267
+ measures,
268
+ }));
269
+
270
+ return segments;
271
+ }
272
+
273
+ getUniqueFacets(data: StatsResponse): string[] {
274
+ const targets = data.measures.map(measure => measure.target);
275
+ return Array.from(new Set(targets));
276
+ }
277
+
278
+ getTargetForKey(key: string): string {
279
+ return key.split(':index:')[1];
280
+ }
281
+
282
+ getTargetForTime(key: string): string {
283
+ return key.split(':index:')[0];
284
+ }
285
+
286
+ async getWorkItems(options: GetStatsOptions, facets: string[]): Promise<string[]> {
287
+ if (!facets.length) {
288
+ const stats = await this.getStats(options);
289
+ facets = this.getUniqueFacets(stats);
290
+ }
291
+ const { key, granularity, range, end, start } = options;
292
+ this.validateOptions(options);
293
+ let redisKeys: string[] = [];
294
+ facets.forEach((facet) => {
295
+ const dateTimeSets = this.generateDateTimeSets(granularity, range, end, start);
296
+ redisKeys = redisKeys.concat(dateTimeSets.map((dateTime) => this.buildRedisKey(key, dateTime, `index:${facet}`)));
297
+ });
298
+ const idsData = await this.store.getJobIds(redisKeys, [0, 1]);
299
+ const workerLists = this.buildWorkerLists(idsData);
300
+ return workerLists;
301
+ }
302
+
303
+ private buildWorkerLists(idsData: IdsData): string[] {
304
+ const workerLists: string[] = [];
305
+ for (const key in idsData) {
306
+ if (idsData[key].length) {
307
+ workerLists.push(key);
308
+ }
309
+ }
310
+ return workerLists;
311
+ }
312
+
313
+ /**
314
+ * called by `trigger` activity to generate the stats that should
315
+ * be saved to the database. doesn't actually save the stats, but
316
+ * just generates the info that should be saved
317
+ */
318
+ resolveTriggerStatistics({ stats: statsConfig}: TriggerActivity, context: JobState): StatsType {
319
+ const stats: StatsType = {
320
+ general: [],
321
+ index: [],
322
+ median: []
323
+ }
324
+ stats.general.push({ metric: 'count', target: 'count', value: 1 });
325
+ for (const measure of statsConfig.measures) {
326
+ const metric = this.resolveMetric({ metric: measure.measure, target: measure.target }, context);
327
+ if (this.isGeneralMetric(measure.measure)) {
328
+ stats.general.push(metric);
329
+ } else if (this.isMedianMetric(measure.measure)) {
330
+ stats.median.push(metric);
331
+ } else if (this.isIndexMetric(measure.measure)) {
332
+ stats.index.push(metric);
333
+ }
334
+ }
335
+ return stats;
336
+ }
337
+
338
+ isGeneralMetric(metric: string): boolean {
339
+ return metric === 'sum' || metric === 'avg' || metric === 'count';
340
+ }
341
+
342
+ isMedianMetric(metric: string): boolean {
343
+ return metric === 'mdn';
344
+ }
345
+
346
+ isIndexMetric(metric: string): boolean {
347
+ return metric === 'index';
348
+ }
349
+
350
+ resolveMetric({metric, target}, context: JobState): StatType {
351
+ const pipe = new Pipe([[target]], context);
352
+ const resolvedValue = pipe.process().toString();
353
+ const resolvedTarget = this.resolveTarget(metric, target, resolvedValue);
354
+ if (metric === 'index') {
355
+ return { metric, target: resolvedTarget, value: context.metadata.jid };
356
+ } else if (metric === 'count') {
357
+ return { metric, target: resolvedTarget, value: 1 };
358
+ }
359
+ return { metric, target: resolvedTarget, value: resolvedValue } as StatType;
360
+ }
361
+
362
+ isCardinalMetric(metric: string): boolean {
363
+ return metric === 'index' || metric === 'count';
364
+ }
365
+
366
+ resolveTarget(metric: string, target: string, resolvedValue: string): string {
367
+ const trimmed = target.substring(1, target.length - 1);
368
+ const trimmedTarget = trimmed.split('.').slice(3).join('/');
369
+ let resolvedTarget: string;
370
+ if (this.isCardinalMetric(metric)) {
371
+ resolvedTarget = `${metric}:${trimmedTarget}:${resolvedValue}`;
372
+ } else {
373
+ resolvedTarget = `${metric}:${trimmedTarget}`;
374
+ }
375
+ return resolvedTarget;
376
+ }
377
+ }
378
+
379
+ export { ReporterService };
@@ -0,0 +1,10 @@
1
+ # Serializer Overview
2
+ ## Sym Keys
3
+ Each activity/job is granted 286 tranche of symbols (26 for metadata keys, 260 for data keys).
4
+
5
+ Typically, 5-10 symbols are used for metadata (aid, atp, etc), but it allows for future expansion. The 260 remaining data symbols allow for 260 unique scalar mapping statements as it is the mapping statements (the paths to reach the target value) define the symbols to use for data mapping (the symbols represent these paths).
6
+
7
+ If `a1` maps to `a1.output.data.abc` and `a2` maps to `a2.output.data.def`, then when `a1` saves its output, it will only save the field values `abc`, and `def`. When saved to the backend it would be: `{a1: {data: { abc: 'somevalue', def: 'another' }}` when flattened and deflated, the keys would be 'bbb' and 'bbc' (or whatever the actual value) and the values would be 'somevalue' and 'another' respectively.
8
+
9
+ ## Sym Values
10
+ It is possible to replace persisted values. Up to 2,704 possible 2-letter symbols are available for use (52^2) to represent any string value.