blackmistcode commited on
Commit
8f7dc55
·
verified ·
1 Parent(s): 0dc7194

Add files using upload-large-folder tool

Browse files
backend/node_modules/effect/src/internal/metric/boundaries.ts ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Arr from "../../Array.js"
2
+ import * as Chunk from "../../Chunk.js"
3
+ import * as Equal from "../../Equal.js"
4
+ import { pipe } from "../../Function.js"
5
+ import * as Hash from "../../Hash.js"
6
+ import type * as MetricBoundaries from "../../MetricBoundaries.js"
7
+ import { pipeArguments } from "../../Pipeable.js"
8
+ import { hasProperty } from "../../Predicate.js"
9
+
10
+ /** @internal */
11
+ const MetricBoundariesSymbolKey = "effect/MetricBoundaries"
12
+
13
+ /** @internal */
14
+ export const MetricBoundariesTypeId: MetricBoundaries.MetricBoundariesTypeId = Symbol.for(
15
+ MetricBoundariesSymbolKey
16
+ ) as MetricBoundaries.MetricBoundariesTypeId
17
+
18
+ /** @internal */
19
+ class MetricBoundariesImpl implements MetricBoundaries.MetricBoundaries {
20
+ readonly [MetricBoundariesTypeId]: MetricBoundaries.MetricBoundariesTypeId = MetricBoundariesTypeId
21
+ constructor(readonly values: ReadonlyArray<number>) {
22
+ this._hash = pipe(
23
+ Hash.string(MetricBoundariesSymbolKey),
24
+ Hash.combine(Hash.array(this.values))
25
+ )
26
+ }
27
+ readonly _hash: number;
28
+ [Hash.symbol](): number {
29
+ return this._hash
30
+ }
31
+ [Equal.symbol](u: unknown): boolean {
32
+ return isMetricBoundaries(u) && Equal.equals(this.values, u.values)
33
+ }
34
+ pipe() {
35
+ return pipeArguments(this, arguments)
36
+ }
37
+ }
38
+
39
+ /** @internal */
40
+ export const isMetricBoundaries = (u: unknown): u is MetricBoundaries.MetricBoundaries =>
41
+ hasProperty(u, MetricBoundariesTypeId)
42
+
43
+ /** @internal */
44
+ export const fromIterable = (iterable: Iterable<number>): MetricBoundaries.MetricBoundaries => {
45
+ const values = pipe(
46
+ iterable,
47
+ Arr.appendAll(Chunk.of(Number.POSITIVE_INFINITY)),
48
+ Arr.dedupe
49
+ )
50
+ return new MetricBoundariesImpl(values)
51
+ }
52
+
53
+ /** @internal */
54
+ export const linear = (options: {
55
+ readonly start: number
56
+ readonly width: number
57
+ readonly count: number
58
+ }): MetricBoundaries.MetricBoundaries =>
59
+ pipe(
60
+ Arr.makeBy(options.count - 1, (i) => options.start + i * options.width),
61
+ Chunk.unsafeFromArray,
62
+ fromIterable
63
+ )
64
+
65
+ /** @internal */
66
+ export const exponential = (options: {
67
+ readonly start: number
68
+ readonly factor: number
69
+ readonly count: number
70
+ }): MetricBoundaries.MetricBoundaries =>
71
+ pipe(
72
+ Arr.makeBy(options.count - 1, (i) => options.start * Math.pow(options.factor, i)),
73
+ Chunk.unsafeFromArray,
74
+ fromIterable
75
+ )
backend/node_modules/effect/src/internal/metric/hook.ts ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Arr from "../../Array.js"
2
+ import * as Duration from "../../Duration.js"
3
+ import type { LazyArg } from "../../Function.js"
4
+ import { dual, pipe } from "../../Function.js"
5
+ import type * as MetricHook from "../../MetricHook.js"
6
+ import type * as MetricKey from "../../MetricKey.js"
7
+ import type * as MetricState from "../../MetricState.js"
8
+ import * as number from "../../Number.js"
9
+ import * as Option from "../../Option.js"
10
+ import { pipeArguments } from "../../Pipeable.js"
11
+ import * as metricState from "./state.js"
12
+
13
+ /** @internal */
14
+ const MetricHookSymbolKey = "effect/MetricHook"
15
+
16
+ /** @internal */
17
+ export const MetricHookTypeId: MetricHook.MetricHookTypeId = Symbol.for(
18
+ MetricHookSymbolKey
19
+ ) as MetricHook.MetricHookTypeId
20
+
21
+ const metricHookVariance = {
22
+ /* c8 ignore next */
23
+ _In: (_: unknown) => _,
24
+ /* c8 ignore next */
25
+ _Out: (_: never) => _
26
+ }
27
+
28
+ /** @internal */
29
+ export const make = <In, Out>(
30
+ options: {
31
+ readonly get: LazyArg<Out>
32
+ readonly update: (input: In) => void
33
+ readonly modify: (input: In) => void
34
+ }
35
+ ): MetricHook.MetricHook<In, Out> => ({
36
+ [MetricHookTypeId]: metricHookVariance,
37
+ pipe() {
38
+ return pipeArguments(this, arguments)
39
+ },
40
+ ...options
41
+ })
42
+
43
+ /** @internal */
44
+ export const onModify = dual<
45
+ <In, Out>(f: (input: In) => void) => (self: MetricHook.MetricHook<In, Out>) => MetricHook.MetricHook<In, Out>,
46
+ <In, Out>(self: MetricHook.MetricHook<In, Out>, f: (input: In) => void) => MetricHook.MetricHook<In, Out>
47
+ >(2, (self, f) => ({
48
+ [MetricHookTypeId]: metricHookVariance,
49
+ pipe() {
50
+ return pipeArguments(this, arguments)
51
+ },
52
+ get: self.get,
53
+ update: self.update,
54
+ modify: (input) => {
55
+ self.modify(input)
56
+ return f(input)
57
+ }
58
+ }))
59
+
60
+ /** @internal */
61
+ export const onUpdate = dual<
62
+ <In, Out>(f: (input: In) => void) => (self: MetricHook.MetricHook<In, Out>) => MetricHook.MetricHook<In, Out>,
63
+ <In, Out>(self: MetricHook.MetricHook<In, Out>, f: (input: In) => void) => MetricHook.MetricHook<In, Out>
64
+ >(2, (self, f) => ({
65
+ [MetricHookTypeId]: metricHookVariance,
66
+ pipe() {
67
+ return pipeArguments(this, arguments)
68
+ },
69
+ get: self.get,
70
+ update: (input) => {
71
+ self.update(input)
72
+ return f(input)
73
+ },
74
+ modify: self.modify
75
+ }))
76
+
77
+ const bigint0 = BigInt(0)
78
+
79
+ /** @internal */
80
+ export const counter = <A extends (number | bigint)>(
81
+ key: MetricKey.MetricKey.Counter<A>
82
+ ): MetricHook.MetricHook.Counter<A> => {
83
+ let sum: A = key.keyType.bigint ? bigint0 as A : 0 as A
84
+ const canUpdate = key.keyType.incremental
85
+ ? key.keyType.bigint
86
+ ? (value: A) => value >= bigint0
87
+ : (value: A) => value >= 0
88
+ : (_value: A) => true
89
+ const update = (value: A) => {
90
+ if (canUpdate(value)) {
91
+ sum = (sum as any) + value
92
+ }
93
+ }
94
+ return make({
95
+ get: () => metricState.counter(sum as number) as unknown as MetricState.MetricState.Counter<A>,
96
+ update,
97
+ modify: update
98
+ })
99
+ }
100
+
101
+ /** @internal */
102
+ export const frequency = (key: MetricKey.MetricKey.Frequency): MetricHook.MetricHook.Frequency => {
103
+ const values = new Map<string, number>()
104
+ for (const word of key.keyType.preregisteredWords) {
105
+ values.set(word, 0)
106
+ }
107
+ const update = (word: string) => {
108
+ const slotCount = values.get(word) ?? 0
109
+ values.set(word, slotCount + 1)
110
+ }
111
+ return make({
112
+ get: () => metricState.frequency(values),
113
+ update,
114
+ modify: update
115
+ })
116
+ }
117
+
118
+ /** @internal */
119
+ export const gauge: {
120
+ (key: MetricKey.MetricKey.Gauge<number>, startAt: number): MetricHook.MetricHook.Gauge<number>
121
+ (key: MetricKey.MetricKey.Gauge<bigint>, startAt: bigint): MetricHook.MetricHook.Gauge<bigint>
122
+ } = <A extends (number | bigint)>(
123
+ _key: MetricKey.MetricKey.Gauge<A>,
124
+ startAt: A
125
+ ): MetricHook.MetricHook.Gauge<A> => {
126
+ let value = startAt
127
+ return make({
128
+ get: () => metricState.gauge(value as number) as unknown as MetricState.MetricState.Gauge<A>,
129
+ update: (v) => {
130
+ value = v
131
+ },
132
+ modify: (v) => {
133
+ value = (value as any) + v
134
+ }
135
+ })
136
+ }
137
+
138
+ /** @internal */
139
+ export const histogram = (key: MetricKey.MetricKey.Histogram): MetricHook.MetricHook.Histogram => {
140
+ const bounds = key.keyType.boundaries.values
141
+ const size = bounds.length
142
+ const values = new Uint32Array(size + 1)
143
+ // NOTE: while 64-bit floating point precision shoule be enough for any
144
+ // practical histogram boundary values, there is still a small chance that
145
+ // precision will be lost with very large / very small numbers. If we find
146
+ // that is the case, a more complex approach storing the histogram boundary
147
+ // values as a tuple of `[original: string, numeric: number]` may be warranted
148
+ const boundaries = new Float64Array(size)
149
+ let count = 0
150
+ let sum = 0
151
+ let min = Number.MAX_VALUE
152
+ let max = Number.MIN_VALUE
153
+
154
+ pipe(
155
+ bounds,
156
+ Arr.sort(number.Order),
157
+ Arr.map((n, i) => {
158
+ boundaries[i] = n
159
+ })
160
+ )
161
+
162
+ // Insert the value into the right bucket with a binary search
163
+ const update = (value: number) => {
164
+ let from = 0
165
+ let to = size
166
+ while (from !== to) {
167
+ const mid = Math.floor(from + (to - from) / 2)
168
+ const boundary = boundaries[mid]
169
+ if (value <= boundary) {
170
+ to = mid
171
+ } else {
172
+ from = mid
173
+ }
174
+ // The special case when to / from have a distance of one
175
+ if (to === from + 1) {
176
+ if (value <= boundaries[from]) {
177
+ to = from
178
+ } else {
179
+ from = to
180
+ }
181
+ }
182
+ }
183
+ values[from] = values[from]! + 1
184
+ count = count + 1
185
+ sum = sum + value
186
+ if (value < min) {
187
+ min = value
188
+ }
189
+ if (value > max) {
190
+ max = value
191
+ }
192
+ }
193
+
194
+ const getBuckets = (): ReadonlyArray<readonly [number, number]> => {
195
+ const builder: Array<readonly [number, number]> = Arr.allocate(size) as any
196
+ let cumulated = 0
197
+ for (let i = 0; i < size; i++) {
198
+ const boundary = boundaries[i]
199
+ const value = values[i]
200
+ cumulated = cumulated + value
201
+ builder[i] = [boundary, cumulated]
202
+ }
203
+ return builder
204
+ }
205
+
206
+ return make({
207
+ get: () =>
208
+ metricState.histogram({
209
+ buckets: getBuckets(),
210
+ count,
211
+ min,
212
+ max,
213
+ sum
214
+ }),
215
+ update,
216
+ modify: update
217
+ })
218
+ }
219
+
220
+ /** @internal */
221
+ export const summary = (key: MetricKey.MetricKey.Summary): MetricHook.MetricHook.Summary => {
222
+ const { error, maxAge, maxSize, quantiles } = key.keyType
223
+ const sortedQuantiles = pipe(quantiles, Arr.sort(number.Order))
224
+ const values = Arr.allocate<readonly [number, number]>(maxSize)
225
+
226
+ let head = 0
227
+ let count = 0
228
+ let sum = 0
229
+ let min = 0
230
+ let max = 0
231
+
232
+ // Just before the snapshot we filter out all values older than maxAge
233
+ const snapshot = (now: number): ReadonlyArray<readonly [number, Option.Option<number>]> => {
234
+ const builder: Array<number> = []
235
+ // If the buffer is not full yet it contains valid items at the 0..last
236
+ // indices and null values at the rest of the positions.
237
+ //
238
+ // If the buffer is already full then all elements contains a valid
239
+ // measurement with timestamp.
240
+ //
241
+ // At any given point in time we can enumerate all the non-null elements in
242
+ // the buffer and filter them by timestamp to get a valid view of a time
243
+ // window.
244
+ //
245
+ // The order does not matter because it gets sorted before passing to
246
+ // `calculateQuantiles`.
247
+ let i = 0
248
+ while (i !== maxSize - 1) {
249
+ const item = values[i]
250
+ if (item != null) {
251
+ const [t, v] = item
252
+ const age = Duration.millis(now - t)
253
+ if (Duration.greaterThanOrEqualTo(age, Duration.zero) && Duration.lessThanOrEqualTo(age, maxAge)) {
254
+ builder.push(v)
255
+ }
256
+ }
257
+ i = i + 1
258
+ }
259
+ return calculateQuantiles(
260
+ error,
261
+ sortedQuantiles,
262
+ Arr.sort(builder, number.Order)
263
+ )
264
+ }
265
+
266
+ const observe = (value: number, timestamp: number) => {
267
+ if (maxSize > 0) {
268
+ head = head + 1
269
+ const target = head % maxSize
270
+ values[target] = [timestamp, value] as const
271
+ }
272
+
273
+ min = count === 0 ? value : Math.min(min, value)
274
+ max = count === 0 ? value : Math.max(max, value)
275
+
276
+ count = count + 1
277
+ sum = sum + value
278
+ }
279
+
280
+ return make({
281
+ get: () =>
282
+ metricState.summary({
283
+ error,
284
+ quantiles: snapshot(Date.now()),
285
+ count,
286
+ min,
287
+ max,
288
+ sum
289
+ }),
290
+ update: ([value, timestamp]) => observe(value, timestamp),
291
+ modify: ([value, timestamp]) => observe(value, timestamp)
292
+ })
293
+ }
294
+
295
+ /** @internal */
296
+ interface ResolvedQuantile {
297
+ /**
298
+ * The quantile that shall be resolved.
299
+ */
300
+ readonly quantile: number
301
+ /**
302
+ * `Some<number>` if a value for the quantile could be found, otherwise
303
+ * `None`.
304
+ */
305
+ readonly value: Option.Option<number>
306
+ /**
307
+ * How many samples have been consumed prior to this quantile.
308
+ */
309
+ readonly consumed: number
310
+ /**
311
+ * The rest of the samples after the quantile has been resolved.
312
+ */
313
+ readonly rest: ReadonlyArray<number>
314
+ }
315
+
316
+ /** @internal */
317
+ const calculateQuantiles = (
318
+ error: number,
319
+ sortedQuantiles: ReadonlyArray<number>,
320
+ sortedSamples: ReadonlyArray<number>
321
+ ): ReadonlyArray<readonly [number, Option.Option<number>]> => {
322
+ // The number of samples examined
323
+ const sampleCount = sortedSamples.length
324
+ if (!Arr.isNonEmptyReadonlyArray(sortedQuantiles)) {
325
+ return Arr.empty()
326
+ }
327
+ const head = sortedQuantiles[0]
328
+ const tail = sortedQuantiles.slice(1)
329
+ const resolvedHead = resolveQuantile(
330
+ error,
331
+ sampleCount,
332
+ Option.none(),
333
+ 0,
334
+ head,
335
+ sortedSamples
336
+ )
337
+ const resolved = Arr.of(resolvedHead)
338
+ tail.forEach((quantile) => {
339
+ resolved.push(
340
+ resolveQuantile(
341
+ error,
342
+ sampleCount,
343
+ resolvedHead.value,
344
+ resolvedHead.consumed,
345
+ quantile,
346
+ resolvedHead.rest
347
+ )
348
+ )
349
+ })
350
+ return Arr.map(resolved, (rq) => [rq.quantile, rq.value] as const)
351
+ }
352
+
353
+ /** @internal */
354
+ const resolveQuantile = (
355
+ error: number,
356
+ sampleCount: number,
357
+ current: Option.Option<number>,
358
+ consumed: number,
359
+ quantile: number,
360
+ rest: ReadonlyArray<number>
361
+ ): ResolvedQuantile => {
362
+ let error_1 = error
363
+ let sampleCount_1 = sampleCount
364
+ let current_1 = current
365
+ let consumed_1 = consumed
366
+ let quantile_1 = quantile
367
+ let rest_1 = rest
368
+ let error_2 = error
369
+ let sampleCount_2 = sampleCount
370
+ let current_2 = current
371
+ let consumed_2 = consumed
372
+ let quantile_2 = quantile
373
+ let rest_2 = rest
374
+ // eslint-disable-next-line no-constant-condition
375
+ while (1) {
376
+ // If the remaining list of samples is empty, there is nothing more to resolve
377
+ if (!Arr.isNonEmptyReadonlyArray(rest_1)) {
378
+ return {
379
+ quantile: quantile_1,
380
+ value: Option.none(),
381
+ consumed: consumed_1,
382
+ rest: []
383
+ }
384
+ }
385
+ // If the quantile is the 100% quantile, we can take the maximum of all the
386
+ // remaining values as the result
387
+ if (quantile_1 === 1) {
388
+ return {
389
+ quantile: quantile_1,
390
+ value: Option.some(Arr.lastNonEmpty(rest_1)),
391
+ consumed: consumed_1 + rest_1.length,
392
+ rest: []
393
+ }
394
+ }
395
+ // Split into two chunks - the first chunk contains all elements of the same
396
+ // value as the chunk head
397
+ const headValue = Arr.headNonEmpty(rest_1) // Get head value since rest_1 is non-empty
398
+ const sameHead = Arr.span(rest_1, (n) => n === headValue)
399
+ // How many elements do we want to accept for this quantile
400
+ const desired = quantile_1 * sampleCount_1
401
+ // The error margin
402
+ const allowedError = (error_1 / 2) * desired
403
+ // Taking into account the elements consumed from the samples so far and the
404
+ // number of same elements at the beginning of the chunk, calculate the number
405
+ // of elements we would have if we selected the current head as result
406
+ const candConsumed = consumed_1 + sameHead[0].length
407
+ const candError = Math.abs(candConsumed - desired)
408
+ // If we haven't got enough elements yet, recurse
409
+ if (candConsumed < desired - allowedError) {
410
+ error_2 = error_1
411
+ sampleCount_2 = sampleCount_1
412
+ current_2 = Arr.head(rest_1)
413
+ consumed_2 = candConsumed
414
+ quantile_2 = quantile_1
415
+ rest_2 = sameHead[1]
416
+ error_1 = error_2
417
+ sampleCount_1 = sampleCount_2
418
+ current_1 = current_2
419
+ consumed_1 = consumed_2
420
+ quantile_1 = quantile_2
421
+ rest_1 = rest_2
422
+ continue
423
+ }
424
+ // If consuming this chunk leads to too many elements (rank is too high)
425
+ if (candConsumed > desired + allowedError) {
426
+ const valueToReturn = Option.isNone(current_1)
427
+ ? Option.some(headValue)
428
+ : current_1
429
+ return {
430
+ quantile: quantile_1,
431
+ value: valueToReturn,
432
+ consumed: consumed_1,
433
+ rest: rest_1
434
+ }
435
+ }
436
+ // If we are in the target interval, select the current head and hand back the leftover after dropping all elements
437
+ // from the sample chunk that are equal to the current head
438
+ switch (current_1._tag) {
439
+ case "None": {
440
+ error_2 = error_1
441
+ sampleCount_2 = sampleCount_1
442
+ current_2 = Arr.head(rest_1)
443
+ consumed_2 = candConsumed
444
+ quantile_2 = quantile_1
445
+ rest_2 = sameHead[1]
446
+ error_1 = error_2
447
+ sampleCount_1 = sampleCount_2
448
+ current_1 = current_2
449
+ consumed_1 = consumed_2
450
+ quantile_1 = quantile_2
451
+ rest_1 = rest_2
452
+ continue
453
+ }
454
+ case "Some": {
455
+ const prevError = Math.abs(desired - current_1.value)
456
+ if (candError < prevError) {
457
+ error_2 = error_1
458
+ sampleCount_2 = sampleCount_1
459
+ current_2 = Arr.head(rest_1)
460
+ consumed_2 = candConsumed
461
+ quantile_2 = quantile_1
462
+ rest_2 = sameHead[1]
463
+ error_1 = error_2
464
+ sampleCount_1 = sampleCount_2
465
+ current_1 = current_2
466
+ consumed_1 = consumed_2
467
+ quantile_1 = quantile_2
468
+ rest_1 = rest_2
469
+ continue
470
+ }
471
+ return {
472
+ quantile: quantile_1,
473
+ value: Option.some(current_1.value),
474
+ consumed: consumed_1,
475
+ rest: rest_1
476
+ }
477
+ }
478
+ }
479
+ }
480
+ throw new Error(
481
+ "BUG: MetricHook.resolveQuantiles - please report an issue at https://github.com/Effect-TS/effect/issues"
482
+ )
483
+ }
backend/node_modules/effect/src/internal/metric/key.ts ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Arr from "../../Array.js"
2
+ import type * as Duration from "../../Duration.js"
3
+ import * as Equal from "../../Equal.js"
4
+ import { dual, pipe } from "../../Function.js"
5
+ import * as Hash from "../../Hash.js"
6
+ import type * as MetricBoundaries from "../../MetricBoundaries.js"
7
+ import type * as MetricKey from "../../MetricKey.js"
8
+ import type * as MetricKeyType from "../../MetricKeyType.js"
9
+ import type * as MetricLabel from "../../MetricLabel.js"
10
+ import * as Option from "../../Option.js"
11
+ import { pipeArguments } from "../../Pipeable.js"
12
+ import { hasProperty } from "../../Predicate.js"
13
+ import * as metricKeyType from "./keyType.js"
14
+ import * as metricLabel from "./label.js"
15
+
16
+ /** @internal */
17
+ const MetricKeySymbolKey = "effect/MetricKey"
18
+
19
+ /** @internal */
20
+ export const MetricKeyTypeId: MetricKey.MetricKeyTypeId = Symbol.for(
21
+ MetricKeySymbolKey
22
+ ) as MetricKey.MetricKeyTypeId
23
+
24
+ const metricKeyVariance = {
25
+ /* c8 ignore next */
26
+ _Type: (_: never) => _
27
+ }
28
+
29
+ const arrayEquivilence = Arr.getEquivalence(Equal.equals)
30
+
31
+ /** @internal */
32
+ class MetricKeyImpl<out Type extends MetricKeyType.MetricKeyType<any, any>> implements MetricKey.MetricKey<Type> {
33
+ readonly [MetricKeyTypeId] = metricKeyVariance
34
+ constructor(
35
+ readonly name: string,
36
+ readonly keyType: Type,
37
+ readonly description: Option.Option<string>,
38
+ readonly tags: ReadonlyArray<MetricLabel.MetricLabel> = []
39
+ ) {
40
+ this._hash = pipe(
41
+ Hash.string(this.name + this.description),
42
+ Hash.combine(Hash.hash(this.keyType)),
43
+ Hash.combine(Hash.array(this.tags))
44
+ )
45
+ }
46
+ readonly _hash: number;
47
+ [Hash.symbol](): number {
48
+ return this._hash
49
+ }
50
+ [Equal.symbol](u: unknown): boolean {
51
+ return isMetricKey(u) &&
52
+ this.name === u.name &&
53
+ Equal.equals(this.keyType, u.keyType) &&
54
+ Equal.equals(this.description, u.description) &&
55
+ arrayEquivilence(this.tags, u.tags)
56
+ }
57
+ pipe() {
58
+ return pipeArguments(this, arguments)
59
+ }
60
+ }
61
+
62
+ /** @internal */
63
+ export const isMetricKey = (u: unknown): u is MetricKey.MetricKey<MetricKeyType.MetricKeyType<unknown, unknown>> =>
64
+ hasProperty(u, MetricKeyTypeId)
65
+
66
+ /** @internal */
67
+ export const counter: {
68
+ (name: string, options?: {
69
+ readonly description?: string | undefined
70
+ readonly bigint?: false | undefined
71
+ readonly incremental?: boolean | undefined
72
+ }): MetricKey.MetricKey.Counter<number>
73
+ (name: string, options: {
74
+ readonly description?: string | undefined
75
+ readonly bigint: true
76
+ readonly incremental?: boolean | undefined
77
+ }): MetricKey.MetricKey.Counter<bigint>
78
+ } = (name: string, options) =>
79
+ new MetricKeyImpl(
80
+ name,
81
+ metricKeyType.counter(options as any),
82
+ Option.fromNullable(options?.description)
83
+ )
84
+
85
+ /** @internal */
86
+ export const frequency = (name: string, options?: {
87
+ readonly description?: string | undefined
88
+ readonly preregisteredWords?: ReadonlyArray<string> | undefined
89
+ }): MetricKey.MetricKey.Frequency =>
90
+ new MetricKeyImpl(name, metricKeyType.frequency(options), Option.fromNullable(options?.description))
91
+
92
+ /** @internal */
93
+ export const gauge: {
94
+ (name: string, options?: {
95
+ readonly description?: string | undefined
96
+ readonly bigint?: false | undefined
97
+ }): MetricKey.MetricKey.Gauge<number>
98
+ (name: string, options: {
99
+ readonly description?: string | undefined
100
+ readonly bigint: true
101
+ }): MetricKey.MetricKey.Gauge<bigint>
102
+ } = (name, options) =>
103
+ new MetricKeyImpl(
104
+ name,
105
+ metricKeyType.gauge(options as any),
106
+ Option.fromNullable(options?.description)
107
+ )
108
+
109
+ /** @internal */
110
+ export const histogram = (
111
+ name: string,
112
+ boundaries: MetricBoundaries.MetricBoundaries,
113
+ description?: string
114
+ ): MetricKey.MetricKey.Histogram =>
115
+ new MetricKeyImpl(
116
+ name,
117
+ metricKeyType.histogram(boundaries),
118
+ Option.fromNullable(description)
119
+ )
120
+
121
+ /** @internal */
122
+ export const summary = (
123
+ options: {
124
+ readonly name: string
125
+ readonly maxAge: Duration.DurationInput
126
+ readonly maxSize: number
127
+ readonly error: number
128
+ readonly quantiles: ReadonlyArray<number>
129
+ readonly description?: string | undefined
130
+ }
131
+ ): MetricKey.MetricKey.Summary =>
132
+ new MetricKeyImpl(
133
+ options.name,
134
+ metricKeyType.summary(options),
135
+ Option.fromNullable(options.description)
136
+ )
137
+
138
+ /** @internal */
139
+ export const tagged = dual<
140
+ (
141
+ key: string,
142
+ value: string
143
+ ) => <Type extends MetricKeyType.MetricKeyType<any, any>>(
144
+ self: MetricKey.MetricKey<Type>
145
+ ) => MetricKey.MetricKey<Type>,
146
+ <Type extends MetricKeyType.MetricKeyType<any, any>>(
147
+ self: MetricKey.MetricKey<Type>,
148
+ key: string,
149
+ value: string
150
+ ) => MetricKey.MetricKey<Type>
151
+ >(3, (self, key, value) => taggedWithLabels(self, [metricLabel.make(key, value)]))
152
+
153
+ /** @internal */
154
+ export const taggedWithLabels = dual<
155
+ (
156
+ extraTags: ReadonlyArray<MetricLabel.MetricLabel>
157
+ ) => <Type extends MetricKeyType.MetricKeyType<any, any>>(
158
+ self: MetricKey.MetricKey<Type>
159
+ ) => MetricKey.MetricKey<Type>,
160
+ <Type extends MetricKeyType.MetricKeyType<any, any>>(
161
+ self: MetricKey.MetricKey<Type>,
162
+ extraTags: ReadonlyArray<MetricLabel.MetricLabel>
163
+ ) => MetricKey.MetricKey<Type>
164
+ >(2, (self, extraTags) =>
165
+ extraTags.length === 0
166
+ ? self
167
+ : new MetricKeyImpl(self.name, self.keyType, self.description, Arr.union(self.tags, extraTags)))
backend/node_modules/effect/src/internal/metric/keyType.ts ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Duration from "../../Duration.js"
2
+ import * as Equal from "../../Equal.js"
3
+ import { pipe } from "../../Function.js"
4
+ import * as Hash from "../../Hash.js"
5
+ import type * as MetricBoundaries from "../../MetricBoundaries.js"
6
+ import type * as MetricKeyType from "../../MetricKeyType.js"
7
+ import { pipeArguments } from "../../Pipeable.js"
8
+ import { hasProperty } from "../../Predicate.js"
9
+
10
+ /** @internal */
11
+ const MetricKeyTypeSymbolKey = "effect/MetricKeyType"
12
+
13
+ /** @internal */
14
+ export const MetricKeyTypeTypeId: MetricKeyType.MetricKeyTypeTypeId = Symbol.for(
15
+ MetricKeyTypeSymbolKey
16
+ ) as MetricKeyType.MetricKeyTypeTypeId
17
+
18
+ /** @internal */
19
+ const CounterKeyTypeSymbolKey = "effect/MetricKeyType/Counter"
20
+
21
+ /** @internal */
22
+ export const CounterKeyTypeTypeId: MetricKeyType.CounterKeyTypeTypeId = Symbol.for(
23
+ CounterKeyTypeSymbolKey
24
+ ) as MetricKeyType.CounterKeyTypeTypeId
25
+
26
+ /** @internal */
27
+ const FrequencyKeyTypeSymbolKey = "effect/MetricKeyType/Frequency"
28
+
29
+ /** @internal */
30
+ export const FrequencyKeyTypeTypeId: MetricKeyType.FrequencyKeyTypeTypeId = Symbol.for(
31
+ FrequencyKeyTypeSymbolKey
32
+ ) as MetricKeyType.FrequencyKeyTypeTypeId
33
+
34
+ /** @internal */
35
+ const GaugeKeyTypeSymbolKey = "effect/MetricKeyType/Gauge"
36
+
37
+ /** @internal */
38
+ export const GaugeKeyTypeTypeId: MetricKeyType.GaugeKeyTypeTypeId = Symbol.for(
39
+ GaugeKeyTypeSymbolKey
40
+ ) as MetricKeyType.GaugeKeyTypeTypeId
41
+
42
+ /** @internal */
43
+ const HistogramKeyTypeSymbolKey = "effect/MetricKeyType/Histogram"
44
+
45
+ /** @internal */
46
+ export const HistogramKeyTypeTypeId: MetricKeyType.HistogramKeyTypeTypeId = Symbol.for(
47
+ HistogramKeyTypeSymbolKey
48
+ ) as MetricKeyType.HistogramKeyTypeTypeId
49
+
50
+ /** @internal */
51
+ const SummaryKeyTypeSymbolKey = "effect/MetricKeyType/Summary"
52
+
53
+ /** @internal */
54
+ export const SummaryKeyTypeTypeId: MetricKeyType.SummaryKeyTypeTypeId = Symbol.for(
55
+ SummaryKeyTypeSymbolKey
56
+ ) as MetricKeyType.SummaryKeyTypeTypeId
57
+
58
+ const metricKeyTypeVariance = {
59
+ /* c8 ignore next */
60
+ _In: (_: unknown) => _,
61
+ /* c8 ignore next */
62
+ _Out: (_: never) => _
63
+ }
64
+
65
+ /** @internal */
66
+ class CounterKeyType<A extends (number | bigint)> implements MetricKeyType.MetricKeyType.Counter<A> {
67
+ readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance
68
+ readonly [CounterKeyTypeTypeId]: MetricKeyType.CounterKeyTypeTypeId = CounterKeyTypeTypeId
69
+ constructor(readonly incremental: boolean, readonly bigint: boolean) {
70
+ this._hash = Hash.string(CounterKeyTypeSymbolKey)
71
+ }
72
+ readonly _hash: number;
73
+ [Hash.symbol](): number {
74
+ return this._hash
75
+ }
76
+ [Equal.symbol](that: unknown): boolean {
77
+ return isCounterKey(that)
78
+ }
79
+ pipe() {
80
+ return pipeArguments(this, arguments)
81
+ }
82
+ }
83
+
84
+ const FrequencyKeyTypeHash = Hash.string(FrequencyKeyTypeSymbolKey)
85
+
86
+ /** @internal */
87
+ class FrequencyKeyType implements MetricKeyType.MetricKeyType.Frequency {
88
+ readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance
89
+ readonly [FrequencyKeyTypeTypeId]: MetricKeyType.FrequencyKeyTypeTypeId = FrequencyKeyTypeTypeId
90
+ constructor(readonly preregisteredWords: ReadonlyArray<string>) {}
91
+ [Hash.symbol](): number {
92
+ return FrequencyKeyTypeHash
93
+ }
94
+ [Equal.symbol](that: unknown): boolean {
95
+ return isFrequencyKey(that)
96
+ }
97
+ pipe() {
98
+ return pipeArguments(this, arguments)
99
+ }
100
+ }
101
+
102
+ const GaugeKeyTypeHash = Hash.string(GaugeKeyTypeSymbolKey)
103
+
104
+ /** @internal */
105
+ class GaugeKeyType<A extends (number | bigint)> implements MetricKeyType.MetricKeyType.Gauge<A> {
106
+ readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance
107
+ readonly [GaugeKeyTypeTypeId]: MetricKeyType.GaugeKeyTypeTypeId = GaugeKeyTypeTypeId
108
+ constructor(readonly bigint: boolean) {}
109
+ [Hash.symbol](): number {
110
+ return GaugeKeyTypeHash
111
+ }
112
+ [Equal.symbol](that: unknown): boolean {
113
+ return isGaugeKey(that)
114
+ }
115
+ pipe() {
116
+ return pipeArguments(this, arguments)
117
+ }
118
+ }
119
+
120
+ /** @internal */
121
+ export class HistogramKeyType implements MetricKeyType.MetricKeyType.Histogram {
122
+ readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance
123
+ readonly [HistogramKeyTypeTypeId]: MetricKeyType.HistogramKeyTypeTypeId = HistogramKeyTypeTypeId
124
+ constructor(readonly boundaries: MetricBoundaries.MetricBoundaries) {
125
+ this._hash = pipe(
126
+ Hash.string(HistogramKeyTypeSymbolKey),
127
+ Hash.combine(Hash.hash(this.boundaries))
128
+ )
129
+ }
130
+ readonly _hash: number;
131
+ [Hash.symbol](): number {
132
+ return this._hash
133
+ }
134
+ [Equal.symbol](that: unknown): boolean {
135
+ return isHistogramKey(that) && Equal.equals(this.boundaries, that.boundaries)
136
+ }
137
+ pipe() {
138
+ return pipeArguments(this, arguments)
139
+ }
140
+ }
141
+
142
+ /** @internal */
143
+ class SummaryKeyType implements MetricKeyType.MetricKeyType.Summary {
144
+ readonly [MetricKeyTypeTypeId] = metricKeyTypeVariance
145
+ readonly [SummaryKeyTypeTypeId]: MetricKeyType.SummaryKeyTypeTypeId = SummaryKeyTypeTypeId
146
+ constructor(
147
+ readonly maxAge: Duration.Duration,
148
+ readonly maxSize: number,
149
+ readonly error: number,
150
+ readonly quantiles: ReadonlyArray<number>
151
+ ) {
152
+ this._hash = pipe(
153
+ Hash.string(SummaryKeyTypeSymbolKey),
154
+ Hash.combine(Hash.hash(this.maxAge)),
155
+ Hash.combine(Hash.hash(this.maxSize)),
156
+ Hash.combine(Hash.hash(this.error)),
157
+ Hash.combine(Hash.array(this.quantiles))
158
+ )
159
+ }
160
+ readonly _hash: number;
161
+ [Hash.symbol](): number {
162
+ return this._hash
163
+ }
164
+ [Equal.symbol](that: unknown): boolean {
165
+ return isSummaryKey(that) &&
166
+ Equal.equals(this.maxAge, that.maxAge) &&
167
+ this.maxSize === that.maxSize &&
168
+ this.error === that.error &&
169
+ Equal.equals(this.quantiles, that.quantiles)
170
+ }
171
+ pipe() {
172
+ return pipeArguments(this, arguments)
173
+ }
174
+ }
175
+
176
+ /** @internal */
177
+ export const counter: <A extends number | bigint>(options?: {
178
+ readonly bigint: boolean
179
+ readonly incremental: boolean
180
+ }) => CounterKeyType<A> = (options) =>
181
+ new CounterKeyType(
182
+ options?.incremental ?? false,
183
+ options?.bigint ?? false
184
+ )
185
+
186
+ /** @internal */
187
+ export const frequency = (options?: {
188
+ readonly preregisteredWords?: ReadonlyArray<string> | undefined
189
+ }): MetricKeyType.MetricKeyType.Frequency => new FrequencyKeyType(options?.preregisteredWords ?? [])
190
+
191
+ /** @internal */
192
+ export const gauge: <A extends number | bigint>(options?: {
193
+ readonly bigint: boolean
194
+ }) => GaugeKeyType<A> = (options) =>
195
+ new GaugeKeyType(
196
+ options?.bigint ?? false
197
+ )
198
+
199
+ /** @internal */
200
+ export const histogram = (boundaries: MetricBoundaries.MetricBoundaries): MetricKeyType.MetricKeyType.Histogram => {
201
+ return new HistogramKeyType(boundaries)
202
+ }
203
+
204
+ /** @internal */
205
+ export const summary = (
206
+ options: {
207
+ readonly maxAge: Duration.DurationInput
208
+ readonly maxSize: number
209
+ readonly error: number
210
+ readonly quantiles: ReadonlyArray<number>
211
+ }
212
+ ): MetricKeyType.MetricKeyType.Summary => {
213
+ return new SummaryKeyType(Duration.decode(options.maxAge), options.maxSize, options.error, options.quantiles)
214
+ }
215
+
216
+ /** @internal */
217
+ export const isMetricKeyType = (u: unknown): u is MetricKeyType.MetricKeyType<unknown, unknown> =>
218
+ hasProperty(u, MetricKeyTypeTypeId)
219
+
220
+ /** @internal */
221
+ export const isCounterKey = (u: unknown): u is MetricKeyType.MetricKeyType.Counter<number | bigint> =>
222
+ hasProperty(u, CounterKeyTypeTypeId)
223
+
224
+ /** @internal */
225
+ export const isFrequencyKey = (u: unknown): u is MetricKeyType.MetricKeyType.Frequency =>
226
+ hasProperty(u, FrequencyKeyTypeTypeId)
227
+
228
+ /** @internal */
229
+ export const isGaugeKey = (u: unknown): u is MetricKeyType.MetricKeyType.Gauge<number | bigint> =>
230
+ hasProperty(u, GaugeKeyTypeTypeId)
231
+
232
+ /** @internal */
233
+ export const isHistogramKey = (u: unknown): u is MetricKeyType.MetricKeyType.Histogram =>
234
+ hasProperty(u, HistogramKeyTypeTypeId)
235
+
236
+ /** @internal */
237
+ export const isSummaryKey = (u: unknown): u is MetricKeyType.MetricKeyType.Summary =>
238
+ hasProperty(u, SummaryKeyTypeTypeId)
backend/node_modules/effect/src/internal/metric/registry.ts ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pipe } from "../../Function.js"
2
+ import type * as MetricHook from "../../MetricHook.js"
3
+ import type * as MetricKey from "../../MetricKey.js"
4
+ import type * as MetricKeyType from "../../MetricKeyType.js"
5
+ import type * as MetricPair from "../../MetricPair.js"
6
+ import type * as MetricRegistry from "../../MetricRegistry.js"
7
+ import * as MutableHashMap from "../../MutableHashMap.js"
8
+ import * as Option from "../../Option.js"
9
+ import * as metricHook from "./hook.js"
10
+ import * as metricKeyType from "./keyType.js"
11
+ import * as metricPair from "./pair.js"
12
+
13
+ /** @internal */
14
+ const MetricRegistrySymbolKey = "effect/MetricRegistry"
15
+
16
+ /** @internal */
17
+ export const MetricRegistryTypeId: MetricRegistry.MetricRegistryTypeId = Symbol.for(
18
+ MetricRegistrySymbolKey
19
+ ) as MetricRegistry.MetricRegistryTypeId
20
+
21
+ /** @internal */
22
+ class MetricRegistryImpl implements MetricRegistry.MetricRegistry {
23
+ readonly [MetricRegistryTypeId]: MetricRegistry.MetricRegistryTypeId = MetricRegistryTypeId
24
+
25
+ private map = MutableHashMap.empty<
26
+ MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>,
27
+ MetricHook.MetricHook.Root
28
+ >()
29
+
30
+ snapshot(): Array<MetricPair.MetricPair.Untyped> {
31
+ const result: Array<MetricPair.MetricPair.Untyped> = []
32
+ for (const [key, hook] of this.map) {
33
+ result.push(metricPair.unsafeMake(key, hook.get()))
34
+ }
35
+ return result
36
+ }
37
+
38
+ get<Type extends MetricKeyType.MetricKeyType<any, any>>(
39
+ key: MetricKey.MetricKey<Type>
40
+ ): MetricHook.MetricHook<
41
+ MetricKeyType.MetricKeyType.InType<typeof key["keyType"]>,
42
+ MetricKeyType.MetricKeyType.OutType<typeof key["keyType"]>
43
+ > {
44
+ const hook = pipe(
45
+ this.map,
46
+ MutableHashMap.get(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>),
47
+ Option.getOrUndefined
48
+ )
49
+ if (hook == null) {
50
+ if (metricKeyType.isCounterKey(key.keyType)) {
51
+ return this.getCounter(key as unknown as MetricKey.MetricKey.Counter<any>) as any
52
+ }
53
+ if (metricKeyType.isGaugeKey(key.keyType)) {
54
+ return this.getGauge(key as unknown as MetricKey.MetricKey.Gauge<any>) as any
55
+ }
56
+ if (metricKeyType.isFrequencyKey(key.keyType)) {
57
+ return this.getFrequency(key as unknown as MetricKey.MetricKey.Frequency) as any
58
+ }
59
+ if (metricKeyType.isHistogramKey(key.keyType)) {
60
+ return this.getHistogram(key as unknown as MetricKey.MetricKey.Histogram) as any
61
+ }
62
+ if (metricKeyType.isSummaryKey(key.keyType)) {
63
+ return this.getSummary(key as unknown as MetricKey.MetricKey.Summary) as any
64
+ }
65
+ throw new Error(
66
+ "BUG: MetricRegistry.get - unknown MetricKeyType - please report an issue at https://github.com/Effect-TS/effect/issues"
67
+ )
68
+ } else {
69
+ return hook as any
70
+ }
71
+ }
72
+
73
+ getCounter<A extends (number | bigint)>(key: MetricKey.MetricKey.Counter<A>): MetricHook.MetricHook.Counter<A> {
74
+ let value = pipe(
75
+ this.map,
76
+ MutableHashMap.get(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>),
77
+ Option.getOrUndefined
78
+ )
79
+ if (value == null) {
80
+ const counter = metricHook.counter(key)
81
+ if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>))) {
82
+ pipe(
83
+ this.map,
84
+ MutableHashMap.set(
85
+ key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>,
86
+ counter as MetricHook.MetricHook.Root
87
+ )
88
+ )
89
+ }
90
+ value = counter
91
+ }
92
+ return value as MetricHook.MetricHook.Counter<A>
93
+ }
94
+
95
+ getFrequency(key: MetricKey.MetricKey.Frequency): MetricHook.MetricHook.Frequency {
96
+ let value = pipe(
97
+ this.map,
98
+ MutableHashMap.get(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>),
99
+ Option.getOrUndefined
100
+ )
101
+ if (value == null) {
102
+ const frequency = metricHook.frequency(key)
103
+ if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>))) {
104
+ pipe(
105
+ this.map,
106
+ MutableHashMap.set(
107
+ key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>,
108
+ frequency as MetricHook.MetricHook.Root
109
+ )
110
+ )
111
+ }
112
+ value = frequency
113
+ }
114
+ return value as MetricHook.MetricHook.Frequency
115
+ }
116
+
117
+ getGauge<A extends (number | bigint)>(key: MetricKey.MetricKey.Gauge<A>): MetricHook.MetricHook.Gauge<A> {
118
+ let value = pipe(
119
+ this.map,
120
+ MutableHashMap.get(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>),
121
+ Option.getOrUndefined
122
+ )
123
+ if (value == null) {
124
+ const gauge = metricHook.gauge(key as any, key.keyType.bigint ? BigInt(0) as any : 0)
125
+ if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>))) {
126
+ pipe(
127
+ this.map,
128
+ MutableHashMap.set(
129
+ key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>,
130
+ gauge as MetricHook.MetricHook.Root
131
+ )
132
+ )
133
+ }
134
+ value = gauge
135
+ }
136
+ return value as MetricHook.MetricHook.Gauge<A>
137
+ }
138
+
139
+ getHistogram(key: MetricKey.MetricKey.Histogram): MetricHook.MetricHook.Histogram {
140
+ let value = pipe(
141
+ this.map,
142
+ MutableHashMap.get(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>),
143
+ Option.getOrUndefined
144
+ )
145
+ if (value == null) {
146
+ const histogram = metricHook.histogram(key)
147
+ if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>))) {
148
+ pipe(
149
+ this.map,
150
+ MutableHashMap.set(
151
+ key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>,
152
+ histogram as MetricHook.MetricHook.Root
153
+ )
154
+ )
155
+ }
156
+ value = histogram
157
+ }
158
+ return value as MetricHook.MetricHook.Histogram
159
+ }
160
+
161
+ getSummary(key: MetricKey.MetricKey.Summary): MetricHook.MetricHook.Summary {
162
+ let value = pipe(
163
+ this.map,
164
+ MutableHashMap.get(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>),
165
+ Option.getOrUndefined
166
+ )
167
+ if (value == null) {
168
+ const summary = metricHook.summary(key)
169
+ if (!pipe(this.map, MutableHashMap.has(key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>))) {
170
+ pipe(
171
+ this.map,
172
+ MutableHashMap.set(
173
+ key as MetricKey.MetricKey<MetricKeyType.MetricKeyType.Untyped>,
174
+ summary as MetricHook.MetricHook.Root
175
+ )
176
+ )
177
+ }
178
+ value = summary
179
+ }
180
+ return value as MetricHook.MetricHook.Summary
181
+ }
182
+ }
183
+
184
+ /** @internal */
185
+ export const make = (): MetricRegistry.MetricRegistry => {
186
+ return new MetricRegistryImpl()
187
+ }
backend/node_modules/effect/src/internal/opCodes/channelChildExecutorDecision.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @internal */
2
+ export const OP_CONTINUE = "Continue" as const
3
+
4
+ /** @internal */
5
+ export type OP_CONTINUE = typeof OP_CONTINUE
6
+
7
+ /** @internal */
8
+ export const OP_CLOSE = "Close" as const
9
+
10
+ /** @internal */
11
+ export type OP_CLOSE = typeof OP_CLOSE
12
+
13
+ /** @internal */
14
+ export const OP_YIELD = "Yield" as const
15
+
16
+ /** @internal */
17
+ export type OP_YIELD = typeof OP_YIELD
backend/node_modules/effect/src/internal/opCodes/channelMergeState.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @internal */
2
+ export const OP_BOTH_RUNNING = "BothRunning" as const
3
+
4
+ /** @internal */
5
+ export type OP_BOTH_RUNNING = typeof OP_BOTH_RUNNING
6
+
7
+ /** @internal */
8
+ export const OP_LEFT_DONE = "LeftDone" as const
9
+
10
+ /** @internal */
11
+ export type OP_LEFT_DONE = typeof OP_LEFT_DONE
12
+
13
+ /** @internal */
14
+ export const OP_RIGHT_DONE = "RightDone" as const
15
+
16
+ /** @internal */
17
+ export type OP_RIGHT_DONE = typeof OP_RIGHT_DONE
backend/node_modules/effect/src/internal/opCodes/effect.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @internal */
2
+ export type OP_ASYNC = typeof OP_ASYNC
3
+
4
+ /** @internal */
5
+ export const OP_ASYNC = "Async" as const
6
+
7
+ /** @internal */
8
+ export type OP_COMMIT = typeof OP_COMMIT
9
+
10
+ /** @internal */
11
+ export const OP_COMMIT = "Commit" as const
12
+
13
+ /** @internal */
14
+ export type OP_FAILURE = typeof OP_FAILURE
15
+
16
+ /** @internal */
17
+ export const OP_FAILURE = "Failure" as const
18
+
19
+ /** @internal */
20
+ export type OP_ON_FAILURE = typeof OP_ON_FAILURE
21
+
22
+ /** @internal */
23
+ export const OP_ON_FAILURE = "OnFailure" as const
24
+
25
+ /** @internal */
26
+ export type OP_ON_SUCCESS = typeof OP_ON_SUCCESS
27
+
28
+ /** @internal */
29
+ export const OP_ON_SUCCESS = "OnSuccess" as const
30
+
31
+ /** @internal */
32
+ export type OP_ON_SUCCESS_AND_FAILURE = typeof OP_ON_SUCCESS_AND_FAILURE
33
+
34
+ /** @internal */
35
+ export const OP_ON_SUCCESS_AND_FAILURE = "OnSuccessAndFailure" as const
36
+
37
+ /** @internal */
38
+ export type OP_SUCCESS = typeof OP_SUCCESS
39
+
40
+ /** @internal */
41
+ export const OP_SUCCESS = "Success" as const
42
+
43
+ /** @internal */
44
+ export type OP_SYNC = typeof OP_SYNC
45
+
46
+ /** @internal */
47
+ export const OP_SYNC = "Sync" as const
48
+
49
+ /** @internal */
50
+ export const OP_TAG = "Tag" as const
51
+
52
+ /** @internal */
53
+ export type OP_TAG = typeof OP_TAG
54
+
55
+ /** @internal */
56
+ export type OP_UPDATE_RUNTIME_FLAGS = typeof OP_UPDATE_RUNTIME_FLAGS
57
+
58
+ /** @internal */
59
+ export const OP_UPDATE_RUNTIME_FLAGS = "UpdateRuntimeFlags" as const
60
+
61
+ /** @internal */
62
+ export type OP_WHILE = typeof OP_WHILE
63
+
64
+ /** @internal */
65
+ export const OP_WHILE = "While" as const
66
+
67
+ /** @internal */
68
+ export type OP_ITERATOR = typeof OP_ITERATOR
69
+
70
+ /** @internal */
71
+ export const OP_ITERATOR = "Iterator" as const
72
+
73
+ /** @internal */
74
+ export type OP_WITH_RUNTIME = typeof OP_WITH_RUNTIME
75
+
76
+ /** @internal */
77
+ export const OP_WITH_RUNTIME = "WithRuntime" as const
78
+
79
+ /** @internal */
80
+ export type OP_YIELD = typeof OP_YIELD
81
+
82
+ /** @internal */
83
+ export const OP_YIELD = "Yield" as const
84
+
85
+ /** @internal */
86
+ export type OP_REVERT_FLAGS = typeof OP_REVERT_FLAGS
87
+
88
+ /** @internal */
89
+ export const OP_REVERT_FLAGS = "RevertFlags" as const
backend/node_modules/effect/src/internal/opCodes/layer.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @internal */
2
+ export const OP_EXTEND_SCOPE = "ExtendScope" as const
3
+
4
+ /** @internal */
5
+ export type OP_EXTEND_SCOPE = typeof OP_EXTEND_SCOPE
6
+
7
+ /** @internal */
8
+ export const OP_FOLD = "Fold" as const
9
+
10
+ /** @internal */
11
+ export type OP_FOLD = typeof OP_FOLD
12
+
13
+ /** @internal */
14
+ export const OP_FRESH = "Fresh" as const
15
+
16
+ /** @internal */
17
+ export type OP_FRESH = typeof OP_FRESH
18
+
19
+ /** @internal */
20
+ export const OP_FROM_EFFECT = "FromEffect" as const
21
+
22
+ /** @internal */
23
+ export type OP_FROM_EFFECT = typeof OP_FROM_EFFECT
24
+
25
+ /** @internal */
26
+ export const OP_SCOPED = "Scoped" as const
27
+
28
+ /** @internal */
29
+ export type OP_SCOPED = typeof OP_SCOPED
30
+
31
+ /** @internal */
32
+ export const OP_SUSPEND = "Suspend" as const
33
+
34
+ /** @internal */
35
+ export type OP_SUSPEND = typeof OP_SUSPEND
36
+
37
+ /** @internal */
38
+ export const OP_PROVIDE = "Provide" as const
39
+
40
+ /** @internal */
41
+ export type OP_PROVIDE = typeof OP_PROVIDE
42
+
43
+ /** @internal */
44
+ export const OP_PROVIDE_MERGE = "ProvideMerge" as const
45
+
46
+ /** @internal */
47
+ export type OP_PROVIDE_MERGE = typeof OP_PROVIDE_MERGE
48
+
49
+ /** @internal */
50
+ export const OP_MERGE_ALL = "MergeAll" as const
51
+
52
+ /** @internal */
53
+ export type OP_MERGE_ALL = typeof OP_MERGE_ALL
54
+
55
+ /** @internal */
56
+ export const OP_ZIP_WITH = "ZipWith" as const
57
+
58
+ /** @internal */
59
+ export type OP_ZIP_WITH = typeof OP_ZIP_WITH
backend/src/app.js ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Aplicacion Express principal — configuracion de middlewares y montaje de rutas.
3
+ *
4
+ * Middlewares aplicados (en orden):
5
+ * 1. helmet() — headers de seguridad (X-Frame-Options, HSTS, etc.).
6
+ * 2. cors() — CORS con origen configurable (CORS_ORIGIN).
7
+ * 3. rateLimit() — 200 peticiones / 15 min por IP.
8
+ * 4. express.json() — parseo de JSON con limite de 1 MB.
9
+ *
10
+ * Rutas REST montadas bajo /api/v1:
11
+ * - /auth → login, perfil (auth.routes.js)
12
+ * - /markets → listado y detalle de mercados (markets.routes.js)
13
+ * - /markets → senales IA por mercado (signals.routes.js, subruta)
14
+ * - /positions → simulador de posiciones virtuales (positions.routes.js)
15
+ * - /watchlist → lista de seguimiento (watchlist.routes.js)
16
+ * - /alerts → historial de alertas (alerts.routes.js)
17
+ * - /health → healthcheck basico
18
+ *
19
+ * Manejo de errores:
20
+ * - notFound → 404 para rutas no definidas.
21
+ * - errorHandler → 500 generico en produccion, detalles en desarrollo.
22
+ */
23
+
24
+ import express from 'express';
25
+ import cors from 'cors';
26
+ import helmet from 'helmet';
27
+ import rateLimit from 'express-rate-limit';
28
+ import { config } from './config.js';
29
+ import { ok } from './utils/apiResponse.js';
30
+ import authRoutes from './auth/auth.routes.js';
31
+ import marketsRoutes from './markets/markets.routes.js';
32
+ import signalsRoutes from './signals/signals.routes.js';
33
+ import positionsRoutes from './positions/positions.routes.js';
34
+ import watchlistRoutes from './watchlist/watchlist.routes.js';
35
+ import alertsRoutes from './alerts/alerts.routes.js';
36
+ import statsRoutes from './stats/stats.routes.js';
37
+ import { notFound } from './middlewares/notFound.js';
38
+ import { errorHandler } from './middlewares/errorHandler.js';
39
+
40
+ const app = express();
41
+
42
+ app.use(helmet());
43
+ app.use(cors({ origin: config.CORS_ORIGIN, credentials: true }));
44
+
45
+ // Rate limit: muy permisivo en desarrollo, restrictivo en producción
46
+ const rateLimitMax = config.NODE_ENV === 'production' ? 200 : 5000;
47
+ app.use(
48
+ rateLimit({
49
+ windowMs: 15 * 60 * 1000,
50
+ max: rateLimitMax,
51
+ standardHeaders: true,
52
+ legacyHeaders: false,
53
+ message: { ok: false, error: { code: 'TOO_MANY_REQUESTS', message: 'Rate limit exceeded' } },
54
+ }),
55
+ );
56
+ app.use(express.json({ limit: '1mb' }));
57
+
58
+ app.get('/api/v1/health', (_req, res) => ok(res, { status: 'up' }));
59
+ app.use('/api/v1/auth', authRoutes);
60
+ app.use('/api/v1/markets', marketsRoutes);
61
+ app.use('/api/v1/markets', signalsRoutes);
62
+ app.use('/api/v1/positions', positionsRoutes);
63
+ app.use('/api/v1/watchlist', watchlistRoutes);
64
+ app.use('/api/v1/alerts', alertsRoutes);
65
+ app.use('/api/v1/stats', statsRoutes);
66
+
67
+ // Servir frontend estático en producción (HuggingFace Spaces)
68
+ if (config.NODE_ENV === 'production') {
69
+ app.use(express.static('../frontend/dist'));
70
+ app.get('*', (_req, res) => {
71
+ res.sendFile('index.html', { root: '../frontend/dist' });
72
+ });
73
+ }
74
+
75
+ app.use(notFound);
76
+ app.use(errorHandler);
77
+
78
+ export default app;
backend/src/auth/auth.controller.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Controladores del modulo de autenticacion.
3
+ *
4
+ * Responsabilidades:
5
+ * - login → recibir credenciales, delegar validacion a auth.service.js,
6
+ * responder con { token, user }.
7
+ * - me → devolver el usuario autenticado extraido del JWT (req.user).
8
+ *
9
+ * Errores:
10
+ * - 401 INVALID_CREDENTIALS → email o password incorrectos (mensaje generico).
11
+ * - 401 UNAUTHORIZED → token invalido o ausente (en requireAuth).
12
+ */
13
+
14
+ import * as authService from './auth.service.js';
15
+ import { ok } from '../utils/apiResponse.js';
16
+
17
+ export const login = async (req, res) => {
18
+ const data = await authService.login(req.body);
19
+ ok(res, data);
20
+ };
21
+
22
+ export const register = async (req, res) => {
23
+ const data = await authService.register(req.body);
24
+ ok(res, data);
25
+ };
26
+
27
+ export const me = async (req, res) => {
28
+ ok(res, { user: req.user });
29
+ };
backend/src/auth/auth.routes.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Rutas REST de autenticacion.
3
+ *
4
+ * Endpoints:
5
+ * POST /api/v1/auth/login
6
+ * → rateLimitLogin (5 intentos / 15 min)
7
+ * → validate(loginSchema)
8
+ * → authController.login
9
+ * → Devuelve JWT + objeto usuario.
10
+ *
11
+ * GET /api/v1/auth/me
12
+ * → requireAuth
13
+ * → authController.me
14
+ * → Devuelve el usuario autenticado (req.user).
15
+ */
16
+
17
+ import { Router } from 'express';
18
+ import * as ctrl from './auth.controller.js';
19
+ import { loginSchema, registerSchema } from './auth.validators.js';
20
+ import { validate } from '../middlewares/validate.js';
21
+ import { requireAuth } from '../middlewares/requireAuth.js';
22
+ import { rateLimitLogin } from '../middlewares/rateLimitLogin.js';
23
+
24
+ const router = Router();
25
+
26
+ router.post('/login', rateLimitLogin, validate(loginSchema), ctrl.login);
27
+ router.post('/register', validate(registerSchema), ctrl.register);
28
+ router.get('/me', requireAuth, ctrl.me);
29
+
30
+ export default router;
backend/src/auth/auth.service.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Logica de negocio del modulo de autenticacion.
3
+ *
4
+ * Responsabilidades:
5
+ * - login({ email, password }) → buscar usuario, comparar hash con bcrypt,
6
+ * verificar que este activo (isActive) y firmar JWT.
7
+ *
8
+ * Seguridad:
9
+ * - Mensaje generico en fallo ("Email or password is incorrect")
10
+ * para no revelar si el email existe.
11
+ * - Bcrypt con salt rounds configurable (BCRYPT_ROUNDS, default 10).
12
+ * - JWT firmado con HS256 y expiracion (JWT_EXPIRES_IN).
13
+ *
14
+ * Devuelve:
15
+ * { token: string, user: { id, email } }
16
+ */
17
+
18
+ import bcrypt from 'bcryptjs';
19
+ import { prisma } from '../utils/prisma.js';
20
+ import { HttpError } from '../utils/apiResponse.js';
21
+ import { signToken } from './jwt.js';
22
+
23
+ const INVALID_CREDENTIALS = new HttpError(401, 'INVALID_CREDENTIALS', 'Email or password is incorrect');
24
+
25
+ export const login = async ({ email, password }) => {
26
+ const user = await prisma.user.findUnique({ where: { email } });
27
+ if (!user || !user.isActive) throw INVALID_CREDENTIALS;
28
+
29
+ const passwordOk = await bcrypt.compare(password, user.passwordHash);
30
+ if (!passwordOk) throw INVALID_CREDENTIALS;
31
+
32
+ const token = signToken({ sub: user.id, email: user.email });
33
+
34
+ return {
35
+ token,
36
+ user: { id: user.id, email: user.email },
37
+ };
38
+ };
39
+
40
+ export const register = async ({ email, password }) => {
41
+ const existing = await prisma.user.findUnique({ where: { email } });
42
+ if (existing) {
43
+ throw new HttpError(409, 'EMAIL_EXISTS', 'Email already registered');
44
+ }
45
+
46
+ const passwordHash = await bcrypt.hash(password, 10);
47
+ const user = await prisma.user.create({
48
+ data: { email, passwordHash, isActive: true },
49
+ });
50
+
51
+ const token = signToken({ sub: user.id, email: user.email });
52
+
53
+ return {
54
+ token,
55
+ user: { id: user.id, email: user.email },
56
+ };
57
+ };
backend/src/auth/auth.validators.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Esquemas Zod para validar inputs del modulo de autenticacion.
3
+ *
4
+ * Responsabilidades:
5
+ * - loginSchema: validar email (formato correcto, lowercase, trim)
6
+ * y password (minimo 8 caracteres).
7
+ *
8
+ * Consumido por:
9
+ * - auth.routes.js → validate(loginSchema) en POST /login.
10
+ */
11
+
12
+ import { z } from 'zod';
13
+
14
+ export const loginSchema = z.object({
15
+ email: z.string().email('Invalid email').toLowerCase().trim(),
16
+ password: z.string().min(8, 'Password must be at least 8 characters'),
17
+ });
18
+
19
+ export const registerSchema = z.object({
20
+ email: z.string().email('Invalid email').toLowerCase().trim(),
21
+ password: z.string().min(8, 'Password must be at least 8 characters'),
22
+ });
backend/src/auth/jwt.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Helpers para firmar y verificar tokens JWT.
3
+ *
4
+ * Responsabilidades:
5
+ * - signToken(payload) → firma un token HS256 con expiracion configurable.
6
+ * - verifyToken(token) → verifica firma, expiracion y algoritmo (solo HS256).
7
+ *
8
+ * Consumido por:
9
+ * - auth.service.js → al hacer login exitoso.
10
+ * - requireAuth.js → en cada peticion protegida.
11
+ *
12
+ * Configuracion:
13
+ * - JWT_SECRET : minimo 32 chars (validado en config.js).
14
+ * - JWT_EXPIRES_IN: default '1h'.
15
+ */
16
+
17
+ import jwt from 'jsonwebtoken';
18
+ import { config } from '../config.js';
19
+
20
+ export const signToken = (payload) =>
21
+ jwt.sign(payload, config.JWT_SECRET, {
22
+ algorithm: 'HS256',
23
+ expiresIn: config.JWT_EXPIRES_IN,
24
+ });
25
+
26
+ export const verifyToken = (token) =>
27
+ jwt.verify(token, config.JWT_SECRET, { algorithms: ['HS256'] });
backend/src/index.js ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Entry point de la aplicacion PolySignal.
3
+ *
4
+ * Inicializa el servidor HTTP nativo de Node.js, monta Express sobre el,
5
+ * configura Socket.io para comunicacion en tiempo real, arranca el scheduler
6
+ * de tareas periodicas (node-cron) y gestiona el cierre limpio ante SIGTERM/SIGINT.
7
+ *
8
+ * Flujo de arranque:
9
+ * 1. Crear servidor HTTP y adjuntar Express (app.js).
10
+ * 2. Inicializar Socket.io con CORS permitido desde CORS_ORIGIN.
11
+ * 3. Conectar el broadcaster (socket/broadcaster.js) para emitir eventos.
12
+ * 4. Escuchar en el puerto configurado (default 7860 para HF Spaces).
13
+ * 5. En modo no-test, iniciar scheduler (sync mercados, senales IA, PnL, alertas).
14
+ *
15
+ * Puerto por defecto: 7860 (requerido por HuggingFace Spaces).
16
+ */
17
+
18
+ import http from 'node:http';
19
+ import { Server as IOServer } from 'socket.io';
20
+ import app from './app.js';
21
+ import { config } from './config.js';
22
+ import { logger } from './utils/logger.js';
23
+ import { prisma } from './utils/prisma.js';
24
+ import { attachBroadcaster } from './socket/broadcaster.js';
25
+ import { startScheduler } from './scheduler.js';
26
+
27
+ const httpServer = http.createServer(app);
28
+ const io = new IOServer(httpServer, { cors: { origin: config.CORS_ORIGIN } });
29
+ attachBroadcaster(io);
30
+
31
+ httpServer.listen(config.PORT, () => {
32
+ logger.info({ port: config.PORT, env: config.NODE_ENV }, 'PolySignal backend up');
33
+ });
34
+
35
+ if (config.NODE_ENV !== 'test') {
36
+ startScheduler();
37
+ }
38
+
39
+ for (const sig of ['SIGTERM', 'SIGINT']) {
40
+ process.on(sig, async () => {
41
+ logger.info({ sig }, 'shutting down');
42
+ httpServer.close();
43
+ await prisma.$disconnect();
44
+ process.exit(0);
45
+ });
46
+ }
backend/src/scheduler.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Scheduler de tareas periodicas usando node-cron.
3
+ *
4
+ * Define y ejecuta los 4 jobs principales del backend:
5
+ * 1. syncMarkets — cada 30s, sincroniza precios desde Polymarket Gamma API.
6
+ * 2. generateSignals — cada 5 min, genera senales IA para el top 20 mercados activos.
7
+ * 3. updatePositionsPnL — cada 30s, recalcula P&L de posiciones abiertas.
8
+ * 4. processAlerts — cada 60s, revisa watchlist y envia alertas por Telegram.
9
+ *
10
+ * Cada job captura sus propios errores para evitar que un fallo afecte a los demas.
11
+ * Se ejecuta inmediatamente al arrancar (llamada manual) y luego segun cron.
12
+ */
13
+
14
+ import { schedule } from 'node-cron';
15
+ import { fetchActiveMarkets } from './markets/polymarket.client.js';
16
+ import { marketsRepository } from './markets/markets.repository.js';
17
+ import { signalsService } from './signals/signals.service.js';
18
+ import { positionsService } from './positions/positions.service.js';
19
+ import { alertsService } from './alerts/alerts.service.js';
20
+ import { emitMarketUpdate } from './socket/broadcaster.js';
21
+ import { logger } from './utils/logger.js';
22
+
23
+ async function syncMarkets() {
24
+ try {
25
+ const markets = await fetchActiveMarkets();
26
+ await Promise.all(markets.map((m) => marketsRepository.upsert(m)));
27
+ // Purga mercados activos que no aparecieron en este sync (restos de syncs previos)
28
+ const deactivated = await marketsRepository.deactivateStale(markets.map((m) => m.id));
29
+ for (const m of markets) {
30
+ emitMarketUpdate({ marketId: m.id, yesPrice: m.yesPrice, noPrice: m.noPrice, volumeEur: m.volumeEur });
31
+ }
32
+ logger.info({ count: markets.length, deactivated }, 'markets synced');
33
+ } catch (err) {
34
+ logger.error({ err: err.message }, 'syncMarkets failed');
35
+ }
36
+ }
37
+
38
+ async function generateSignals() {
39
+ try {
40
+ // Seleccion diversificada por categoria + liquidez (40 mercados/ciclo)
41
+ const markets = await marketsRepository.findDiversified(40);
42
+ const byCategory = markets.reduce((acc, m) => {
43
+ acc[m.category] = (acc[m.category] || 0) + 1;
44
+ return acc;
45
+ }, {});
46
+ logger.info({ total: markets.length, byCategory }, 'generating signals for diversified set');
47
+
48
+ for (const market of markets) {
49
+ try {
50
+ await signalsService.generateForMarket(market);
51
+ } catch (err) {
52
+ logger.error({ err: err.message, marketId: market.id }, 'signal generation failed for market');
53
+ }
54
+ }
55
+ } catch (err) {
56
+ logger.error({ err: err.message }, 'generateSignals failed');
57
+ }
58
+ }
59
+
60
+ async function updatePositionsPnL() {
61
+ try {
62
+ await positionsService.updateAllPnL();
63
+ } catch (err) {
64
+ logger.error({ err: err.message }, 'updatePositionsPnL failed');
65
+ }
66
+ }
67
+
68
+ async function processAlerts() {
69
+ try {
70
+ await alertsService.processAll();
71
+ } catch (err) {
72
+ logger.error({ err: err.message }, 'processAlerts failed');
73
+ }
74
+ }
75
+
76
+ export function startScheduler() {
77
+ syncMarkets();
78
+ schedule('*/30 * * * * *', syncMarkets);
79
+ schedule('*/5 * * * *', generateSignals);
80
+ schedule('*/30 * * * * *', updatePositionsPnL);
81
+ schedule('* * * * *', processAlerts);
82
+ logger.info('scheduler started');
83
+ }
backend/src/signals/signals.controller.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Controladores del modulo de senales IA.
3
+ *
4
+ * Responsabilidades:
5
+ * - getLatest(req, res) → devuelve la senal mas reciente de un mercado.
6
+ *
7
+ * Endpoint:
8
+ * GET /api/v1/markets/:marketId/signal
9
+ *
10
+ * Errores:
11
+ * - 404 NOT_FOUND si el mercado no existe.
12
+ * - 404 NOT_FOUND si aun no hay senal generada para ese mercado.
13
+ */
14
+
15
+ import { ok } from '../utils/apiResponse.js';
16
+ import { signalsService } from './signals.service.js';
17
+
18
+ export const signalsController = {
19
+ async getLatest(req, res) {
20
+ const signal = await signalsService.getLatest(req.params.marketId);
21
+ ok(res, signal);
22
+ },
23
+
24
+ async getLatestBatch(req, res) {
25
+ const ids = req.query.marketIds;
26
+ const marketIds = ids ? ids.split(',').filter(Boolean) : [];
27
+ const signals = await signalsService.getLatestBatch(marketIds);
28
+ ok(res, signals);
29
+ },
30
+ };
backend/src/signals/signals.repository.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Repositorio de acceso a datos para el modelo AISignal.
3
+ *
4
+ * Responsabilidades:
5
+ * - create(data) → inserta una nueva senal generada por IA.
6
+ * - findLatestByMarket(id) → devuelve la senal mas reciente de un mercado.
7
+ *
8
+ * Campos persistidos:
9
+ * signal, confidence, summary, keyRisk, newsCount, modelVersion, generatedAt.
10
+ *
11
+ * Todas las operaciones usan Prisma ORM.
12
+ */
13
+
14
+ import { prisma } from '../utils/prisma.js';
15
+
16
+ export const signalsRepository = {
17
+ create({ marketId, signal, confidence, summary, keyRisk, newsCount, modelVersion, impliedProb, fairProb, edgePoints }) {
18
+ return prisma.aISignal.create({
19
+ data: { marketId, signal, confidence, summary, keyRisk, newsCount, modelVersion, impliedProb, fairProb, edgePoints },
20
+ });
21
+ },
22
+
23
+ findLatestByMarket(marketId) {
24
+ return prisma.aISignal.findFirst({
25
+ where: { marketId },
26
+ orderBy: { generatedAt: 'desc' },
27
+ });
28
+ },
29
+
30
+ findLatestForMarkets(marketIds) {
31
+ if (!marketIds || marketIds.length === 0) return Promise.resolve([]);
32
+ return prisma.aISignal.findMany({
33
+ where: { marketId: { in: marketIds } },
34
+ orderBy: [{ marketId: 'asc' }, { generatedAt: 'desc' }],
35
+ }).then((rows) => {
36
+ // Tomar solo la más reciente por marketId
37
+ const seen = new Set();
38
+ return rows.filter((r) => {
39
+ if (seen.has(r.marketId)) return false;
40
+ seen.add(r.marketId);
41
+ return true;
42
+ });
43
+ });
44
+ },
45
+ };
backend/src/signals/signals.routes.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Rutas REST del modulo de senales IA.
3
+ *
4
+ * Endpoint (montado en /api/v1/markets):
5
+ * GET /:marketId/signal → senal mas reciente del mercado.
6
+ *
7
+ * No requiere autenticacion (datos publicos).
8
+ */
9
+
10
+ import { Router } from 'express';
11
+ import { signalsController } from './signals.controller.js';
12
+
13
+ const router = Router();
14
+
15
+ // Batch: últimas señales para múltiples mercados (debe ir ANTES de la ruta dinámica)
16
+ // mounted at /api/v1/markets → full path: GET /api/v1/markets/signals/latest?marketIds=id1,id2
17
+ router.get('/signals/latest', signalsController.getLatestBatch);
18
+
19
+ // mounted at /api/v1/markets → full path: GET /api/v1/markets/:marketId/signal
20
+ router.get('/:marketId/signal', signalsController.getLatest);
21
+
22
+ export default router;