Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions apps/alerting/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
import * as MapleCloudflareSDK from "@maple-dev/effect-sdk/cloudflare"
import {
runScheduledEffect,
WorkerConfigProviderLive,
WorkerEnvironmentLive,
WorkerConfigProviderLayer,
WorkerEnvironment,
} from "@maple/effect-cloudflare"
import { Cause, Effect, Layer } from "effect"

Expand All @@ -31,16 +31,16 @@ import { Cause, Effect, Layer } from "effect"
const telemetry = MapleCloudflareSDK.make({ serviceName: "alerting" })

const buildLayer = (_env: Record<string, unknown>) => {
const ConfigLive = WorkerConfigProviderLive
const EnvLive = Env.Default.pipe(Layer.provide(ConfigLive))
const ConfigLive = WorkerConfigProviderLayer
const EnvLive = Env.layer.pipe(Layer.provide(ConfigLive))

const DatabaseLive = DatabaseD1Live.pipe(Layer.provide(WorkerEnvironmentLive))
const DatabaseLive = DatabaseD1Live.pipe(Layer.provide(WorkerEnvironment.layer))

const BaseLive = Layer.mergeAll(EnvLive, DatabaseLive)

const OrgClickHouseSettingsLive = OrgClickHouseSettingsService.Live.pipe(Layer.provide(BaseLive))
const OrgClickHouseSettingsLive = OrgClickHouseSettingsService.layer.pipe(Layer.provide(BaseLive))

const WarehouseQueryServiceLive = WarehouseQueryService.Live.pipe(
const WarehouseQueryServiceLive = WarehouseQueryService.layer.pipe(
Layer.provide(Layer.mergeAll(EnvLive, OrgClickHouseSettingsLive)),
)

Expand All @@ -52,37 +52,37 @@ const buildLayer = (_env: Record<string, unknown>) => {
Layer.provide(BucketCacheServiceLive),
)

const HazelOAuthServiceLive = HazelOAuthService.Live.pipe(Layer.provide(BaseLive))
const HazelOAuthServiceLive = HazelOAuthService.layer.pipe(Layer.provide(BaseLive))

const AlertsServiceLive = AlertsService.Live.pipe(
const AlertsServiceLive = AlertsService.layer.pipe(
Layer.provide(
Layer.mergeAll(
BaseLive,
QueryEngineServiceLive,
WarehouseQueryServiceLive,
AlertRuntime.Default,
AlertRuntime.layer,
HazelOAuthServiceLive,
),
),
)

const NotificationDispatcherLive = NotificationDispatcher.Live.pipe(
const NotificationDispatcherLive = NotificationDispatcher.layer.pipe(
Layer.provide(Layer.mergeAll(BaseLive, HazelOAuthServiceLive)),
)

const ErrorsServiceLive = ErrorsService.Live.pipe(
const ErrorsServiceLive = ErrorsService.layer.pipe(
Layer.provide(Layer.mergeAll(BaseLive, WarehouseQueryServiceLive, NotificationDispatcherLive)),
)

const EmailServiceLive = EmailService.Default.pipe(Layer.provide(EnvLive))
const EmailServiceLive = EmailService.layer.pipe(Layer.provide(EnvLive))

const DigestServiceLive = DigestService.Default.pipe(
const DigestServiceLive = DigestService.layer.pipe(
Layer.provide(Layer.mergeAll(BaseLive, WarehouseQueryServiceLive, EmailServiceLive)),
)

const OnboardingServiceLive = OnboardingService.Live.pipe(Layer.provide(BaseLive))
const OnboardingServiceLive = OnboardingService.layer.pipe(Layer.provide(BaseLive))

const OnboardingEmailServiceLive = OnboardingEmailService.Live.pipe(
const OnboardingEmailServiceLive = OnboardingEmailService.layer.pipe(
Layer.provide(
Layer.mergeAll(
BaseLive,
Expand All @@ -93,7 +93,7 @@ const buildLayer = (_env: Record<string, unknown>) => {
),
)

const ServiceMapRollupServiceLive = ServiceMapRollupService.Live.pipe(
const ServiceMapRollupServiceLive = ServiceMapRollupService.layer.pipe(
Layer.provide(Layer.mergeAll(BaseLive, WarehouseQueryServiceLive)),
)

Expand Down
16 changes: 8 additions & 8 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { HazelOAuthService } from "./services/HazelOAuthService"
import { NotificationDispatcher } from "./services/NotificationDispatcher"
import { ApiKeysService } from "./services/ApiKeysService"
import { AuthService } from "./services/AuthService"
import { AuthorizationLive } from "./services/AuthorizationLive"
import { ApiAuthorizationLayer } from "./services/ApiAuthorizationLayer"
import { CloudflareLogpushService } from "./services/CloudflareLogpushService"
import { DashboardPersistenceService } from "./services/DashboardPersistenceService"
import { DemoService } from "./services/DemoService"
Expand Down Expand Up @@ -62,7 +62,7 @@ export const DocsRoute = HttpApiScalar.layer(MapleApi, {
path: "/docs",
})

export const InfraLive = Env.Default
export const InfraLive = Env.layer

export const CoreServicesLive = Layer.mergeAll(
AuthService.layer,
Expand Down Expand Up @@ -94,7 +94,7 @@ export const QueryEngineServiceLive = QueryEngineService.layer.pipe(
)

export const AlertsServiceLive = AlertsService.layer.pipe(
Layer.provideMerge(Layer.mergeAll(CoreServicesLive, QueryEngineServiceLive, AlertRuntime.Default)),
Layer.provideMerge(Layer.mergeAll(CoreServicesLive, QueryEngineServiceLive, AlertRuntime.layer)),
)

export const NotificationDispatcherLive = NotificationDispatcher.layer.pipe(
Expand All @@ -105,9 +105,9 @@ export const ErrorsServiceLive = ErrorsService.layer.pipe(
Layer.provideMerge(Layer.mergeAll(CoreServicesLive, WarehouseQueryServiceLive, NotificationDispatcherLive)),
)

export const EmailServiceLive = EmailService.Default.pipe(Layer.provide(Env.Default))
export const EmailServiceLive = EmailService.layer.pipe(Layer.provide(Env.layer))

export const DigestServiceLive = DigestService.Default.pipe(
export const DigestServiceLive = DigestService.layer.pipe(
Layer.provideMerge(Layer.mergeAll(InfraLive, WarehouseQueryServiceLive, EmailServiceLive)),
)

Expand All @@ -119,7 +119,7 @@ export const MainLive = Layer.mergeAll(
ErrorsServiceLive,
DigestServiceLive,
DemoServiceLive,
RawSqlChartService.Default,
RawSqlChartService.layer,
)

export const ApiRoutes = HttpApiBuilder.layer(MapleApi).pipe(
Expand Down Expand Up @@ -165,9 +165,9 @@ export const AllRoutes = Layer.mergeAll(
),
)

export const ApiAuthLive = AuthorizationLive.pipe(
export const ApiAuthLive = ApiAuthorizationLayer.pipe(
Layer.provideMerge(ApiKeysService.layer),
Layer.provideMerge(Env.Default),
Layer.provideMerge(Env.layer),
)

// The OTLP tracer/logger is built per-request in worker.ts and injected via
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ const testConfig = (url: string) =>
)

const makeLayer = (url: string) =>
DashboardPersistenceService.Live.pipe(
DashboardPersistenceService.layer.pipe(
Layer.provide(DatabaseLibsqlLive),
Layer.provide(Env.Default),
Layer.provide(Env.layer),
Layer.provide(testConfig(url)),
)

Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/services/AlertDeliveryDispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type AlertSignalType,
} from "@maple/domain/http"
import type { AlertDestinationRow } from "@maple/db"
import { Duration, Effect, Match, Option } from "effect"
import { Clock, Duration, Effect, Match, Option } from "effect"
import type { EnrichedDestinationSecretConfig } from "./AlertDestinationHydration"
import { safeFetch } from "../lib/url-validator"

Expand Down Expand Up @@ -449,7 +449,7 @@ export const dispatchDelivery = (
},
linkUrl,
chatUrl,
sentAt: new Date().toISOString(),
sentAt: new Date(yield* Clock.currentTimeMillis).toISOString(),
})
const response = yield* runTimedFetch(
"hazel-oauth",
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/services/AlertsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const makeLayer = (
runtimeOverrides?: Partial<AlertRuntimeShape>,
) => {
const configLive = makeConfig(url)
const envLive = Env.Default.pipe(Layer.provide(configLive))
const envLive = Env.layer.pipe(Layer.provide(configLive))
const databaseLive = DatabaseLibsqlLive.pipe(Layer.provide(envLive))
const warehouseLive = Layer.succeed(WarehouseQueryService, warehouseStub)
const bucketCacheLive = BucketCacheService.layer.pipe(Layer.provide(EdgeCacheService.layer))
Expand All @@ -109,9 +109,9 @@ const makeLayer = (
Layer.provide(bucketCacheLive),
)
const runtimeLive = Layer.succeed(AlertRuntime, { ...defaultTestRuntime, ...runtimeOverrides })
const hazelOAuthLive = HazelOAuthService.Live.pipe(Layer.provide(Layer.mergeAll(envLive, databaseLive)))
const hazelOAuthLive = HazelOAuthService.layer.pipe(Layer.provide(Layer.mergeAll(envLive, databaseLive)))

return AlertsService.Live.pipe(
return AlertsService.layer.pipe(
Layer.provide(
Layer.mergeAll(envLive, databaseLive, queryEngineLive, warehouseLive, runtimeLive, hazelOAuthLive),
),
Expand Down
31 changes: 16 additions & 15 deletions apps/api/src/services/AlertsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
Match,
Metric,
Option,
Random,
Redacted,
Ref,
Schema,
Expand Down Expand Up @@ -317,17 +318,15 @@ export interface AlertRuntimeShape {
readonly deliveryTimeoutMs: () => number
}

export class AlertRuntime extends Context.Service<AlertRuntime, AlertRuntimeShape>()("AlertRuntime", {
export class AlertRuntime extends Context.Service<AlertRuntime, AlertRuntimeShape>()("@maple/api/services/AlertRuntime", {
make: Effect.succeed({
now: () => Date.now(),
makeUuid: () => randomUUID(),
fetch: globalThis.fetch as typeof fetch,
deliveryTimeoutMs: () => DELIVERY_TIMEOUT_MS_DEFAULT,
}),
now: () => Date.now(),
makeUuid: () => randomUUID(),
fetch: globalThis.fetch as typeof fetch,
deliveryTimeoutMs: () => DELIVERY_TIMEOUT_MS_DEFAULT,
} satisfies AlertRuntimeShape),
}) {
static readonly layer = Layer.effect(this, this.make)
static readonly Live = this.layer
static readonly Default = this.layer
}

const toIso = (value: number | null | undefined): IsoDateTimeValue | null =>
Expand Down Expand Up @@ -947,7 +946,7 @@ export interface AlertsServiceShape {
>
}

export class AlertsService extends Context.Service<AlertsService, AlertsServiceShape>()("AlertsService", {
export class AlertsService extends Context.Service<AlertsService, AlertsServiceShape>()("@maple/api/services/AlertsService", {
make: Effect.gen(function* () {
const database = yield* Database
const env = yield* Env
Expand Down Expand Up @@ -1637,11 +1636,15 @@ export class AlertsService extends Context.Service<AlertsService, AlertsServiceS
})
}

const computeRetryDelayMs = (attemptNumber: number) => {
// Exponential backoff up to 15 min, plus 0–999 ms jitter sourced from
// Effect's `Random` service so tests can fix the seed deterministically.
const computeRetryDelayMs = Effect.fn("AlertsService.computeRetryDelayMs")(function* (
attemptNumber: number,
) {
const base = Math.min(60_000 * Math.pow(2, attemptNumber - 1), 15 * 60_000)
const jitter = Math.floor(Math.random() * 1_000)
const jitter = yield* Random.nextIntBetween(0, 1_000)
return base + jitter
}
})

const listDestinations = Effect.fn("AlertsService.listDestinations")(function* (orgId: OrgId) {
const rows = yield* dbExecute((db) =>
Expand Down Expand Up @@ -2795,7 +2798,7 @@ export class AlertsService extends Context.Service<AlertsService, AlertsServiceS
decodeAlertDestinationIdSync(row.destinationId),
decodeAlertEventTypeSync(row.eventType),
retryPayload,
currentTime + computeRetryDelayMs(row.attemptNumber),
currentTime + (yield* computeRetryDelayMs(row.attemptNumber)),
row.deliveryKey,
row.attemptNumber + 1,
)
Expand Down Expand Up @@ -3594,6 +3597,4 @@ export class AlertsService extends Context.Service<AlertsService, AlertsServiceS
}),
}) {
static readonly layer = Layer.effect(this, this.make)
static readonly Live = this.layer
static readonly Default = this.layer
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const getBearerToken = (headers: Record<string, string | undefined>): string | u
return token
}

export const AuthorizationLive = Layer.effect(
export const ApiAuthorizationLayer = Layer.effect(
CurrentTenant.Authorization,
Effect.gen(function* () {
const env = yield* Env
Expand Down
18 changes: 10 additions & 8 deletions apps/api/src/services/ApiKeysService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@maple/domain/http"
import { API_KEY_PREFIX, apiKeys, generateApiKey, hashApiKey, parseIngestKeyLookupHmacKey } from "@maple/db"
import { and, desc, eq } from "drizzle-orm"
import { Effect, Layer, Option, Redacted, Schema, Context } from "effect"
import { Clock, Effect, Layer, Option, Redacted, Schema, Context } from "effect"
import { Database } from "./DatabaseLive"
import { Env } from "./Env"

Expand Down Expand Up @@ -48,7 +48,7 @@ const rowToResponse = (row: typeof apiKeys.$inferSelect): ApiKeyResponse =>
createdByEmail: row.createdByEmail ?? null,
})

export class ApiKeysService extends Context.Service<ApiKeysService>()("ApiKeysService", {
export class ApiKeysService extends Context.Service<ApiKeysService>()("@maple/api/services/ApiKeysService", {
make: Effect.gen(function* () {
const database = yield* Database
const env = yield* Env
Expand Down Expand Up @@ -109,7 +109,7 @@ export class ApiKeysService extends Context.Service<ApiKeysService>()("ApiKeysSe
const rawKey = generateApiKey()
const keyHash = hashApiKey(rawKey, hmacKey)
const keyPrefix = rawKey.slice(0, 12) + "..."
const now = Date.now()
const now = yield* Clock.currentTimeMillis
const expiresAt = params.expiresInSeconds ? now + params.expiresInSeconds * 1000 : undefined
const kind: ApiKeyKind = params.kind ?? "standard"
const createdByEmail = params.createdByEmail ?? null
Expand Down Expand Up @@ -150,7 +150,7 @@ export class ApiKeysService extends Context.Service<ApiKeysService>()("ApiKeysSe
})

const revoke = Effect.fn("ApiKeysService.revoke")(function* (orgId: OrgId, keyId: ApiKeyId) {
const now = Date.now()
const now = yield* Clock.currentTimeMillis
const row = yield* requireById(orgId, keyId)

yield* database
Expand All @@ -173,7 +173,10 @@ export class ApiKeysService extends Context.Service<ApiKeysService>()("ApiKeysSe
const row = Option.fromNullishOr(rows[0])
if (Option.isNone(row)) return Option.none()
if (row.value.revoked) return Option.none()
if (row.value.expiresAt && row.value.expiresAt < Date.now()) return Option.none()
if (row.value.expiresAt) {
const now = yield* Clock.currentTimeMillis
if (row.value.expiresAt < now) return Option.none()
}

return Option.some({
orgId: decodeOrgIdSync(row.value.orgId),
Expand All @@ -184,9 +187,10 @@ export class ApiKeysService extends Context.Service<ApiKeysService>()("ApiKeysSe
})

const touchLastUsed = Effect.fn("ApiKeysService.touchLastUsed")(function* (keyId: ApiKeyId) {
const now = yield* Clock.currentTimeMillis
yield* database
.execute((db) =>
db.update(apiKeys).set({ lastUsedAt: Date.now() }).where(eq(apiKeys.id, keyId)),
db.update(apiKeys).set({ lastUsedAt: now }).where(eq(apiKeys.id, keyId)),
)
.pipe(Effect.mapError(toPersistenceError))
})
Expand Down Expand Up @@ -216,6 +220,4 @@ export class ApiKeysService extends Context.Service<ApiKeysService>()("ApiKeysSe
}),
}) {
static readonly layer = Layer.effect(this, this.make)
static readonly Live = this.layer
static readonly Default = this.layer
}
Loading
Loading