Add files using upload-large-folder tool
Browse files- backend/node_modules/effect/src/internal/metric/boundaries.ts +75 -0
- backend/node_modules/effect/src/internal/metric/hook.ts +483 -0
- backend/node_modules/effect/src/internal/metric/key.ts +167 -0
- backend/node_modules/effect/src/internal/metric/keyType.ts +238 -0
- backend/node_modules/effect/src/internal/metric/registry.ts +187 -0
- backend/node_modules/effect/src/internal/opCodes/channelChildExecutorDecision.ts +17 -0
- backend/node_modules/effect/src/internal/opCodes/channelMergeState.ts +17 -0
- backend/node_modules/effect/src/internal/opCodes/effect.ts +89 -0
- backend/node_modules/effect/src/internal/opCodes/layer.ts +59 -0
- backend/src/app.js +78 -0
- backend/src/auth/auth.controller.js +29 -0
- backend/src/auth/auth.routes.js +30 -0
- backend/src/auth/auth.service.js +57 -0
- backend/src/auth/auth.validators.js +22 -0
- backend/src/auth/jwt.js +27 -0
- backend/src/index.js +46 -0
- backend/src/scheduler.js +83 -0
- backend/src/signals/signals.controller.js +30 -0
- backend/src/signals/signals.repository.js +45 -0
- backend/src/signals/signals.routes.js +22 -0
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;
|