better-ship 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{application-rpZoTfzU.js → application-DjsQtpgw.js} +2 -2
- package/dist/application-DjsQtpgw.js.map +1 -0
- package/dist/application.d.ts +3 -2
- package/dist/application.js +1 -1
- package/dist/better-auth/apple-client-secret.d.ts +21 -0
- package/dist/better-auth/apple-client-secret.d.ts.map +1 -0
- package/dist/better-auth/apple-client-secret.js +44 -0
- package/dist/better-auth/apple-client-secret.js.map +1 -0
- package/dist/better-auth/auth-client.d.ts +16 -0
- package/dist/better-auth/auth-client.d.ts.map +1 -0
- package/dist/better-auth/auth-client.js +24 -0
- package/dist/better-auth/auth-client.js.map +1 -0
- package/dist/better-auth/rate-limit.d.ts +34 -0
- package/dist/better-auth/rate-limit.d.ts.map +1 -0
- package/dist/better-auth/rate-limit.js +59 -0
- package/dist/better-auth/rate-limit.js.map +1 -0
- package/dist/better-auth/secondary-storage.d.ts +11 -0
- package/dist/better-auth/secondary-storage.d.ts.map +1 -0
- package/dist/better-auth/secondary-storage.js +18 -0
- package/dist/better-auth/secondary-storage.js.map +1 -0
- package/dist/better-auth/stripe.d.ts +9 -0
- package/dist/better-auth/stripe.d.ts.map +1 -0
- package/dist/better-auth/stripe.js +17 -0
- package/dist/better-auth/stripe.js.map +1 -0
- package/dist/better-auth.d.ts +100 -0
- package/dist/better-auth.d.ts.map +1 -0
- package/dist/better-auth.js +112 -0
- package/dist/better-auth.js.map +1 -0
- package/dist/cloudflare/auth-secondary-storage.d.ts +36 -0
- package/dist/cloudflare/auth-secondary-storage.d.ts.map +1 -0
- package/dist/cloudflare/auth-secondary-storage.js +98 -0
- package/dist/cloudflare/auth-secondary-storage.js.map +1 -0
- package/dist/cloudflare.d.ts +7 -7
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +3 -3
- package/dist/cloudflare.js.map +1 -1
- package/dist/core-Cx5SeyKV.js +709 -0
- package/dist/core-Cx5SeyKV.js.map +1 -0
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/index-BStnggQO.d.ts +584 -0
- package/dist/index-BStnggQO.d.ts.map +1 -0
- package/dist/{index-BN_tjxME.d.ts → index-DWIRrJMP.d.ts} +21 -50
- package/dist/index-DWIRrJMP.d.ts.map +1 -0
- package/dist/postgres.d.ts +7 -7
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +3 -3
- package/dist/postgres.js.map +1 -1
- package/dist/tanstack.d.ts +88 -1
- package/dist/tanstack.d.ts.map +1 -0
- package/dist/tanstack.js +225 -0
- package/dist/tanstack.js.map +1 -0
- package/dist/{unit-of-work-context-CbUJGHlL.d.ts → unit-of-work-context-CPz9rdSS.d.ts} +3 -2
- package/dist/unit-of-work-context-CPz9rdSS.d.ts.map +1 -0
- package/dist/{unit-of-work-context-BLNj72v0.js → unit-of-work-context-DToDpAkv.js} +2 -2
- package/dist/unit-of-work-context-DToDpAkv.js.map +1 -0
- package/package.json +57 -12
- package/src/application/event-collector.ts +3 -3
- package/src/application/handlers.ts +7 -4
- package/src/application/index.ts +1 -1
- package/src/application/message-already-processed.error.ts +1 -3
- package/src/application/message-bus.ts +14 -8
- package/src/application/message-store.ts +2 -2
- package/src/application/registry.ts +14 -13
- package/src/application/unit-of-work.ts +2 -1
- package/src/core/authentication.error.ts +11 -0
- package/src/core/define-messages.ts +136 -0
- package/src/core/entity.ts +33 -0
- package/src/core/error-table.ts +188 -0
- package/src/core/identity.ts +29 -0
- package/src/core/index.ts +50 -0
- package/src/core/internal.errors.ts +21 -0
- package/src/core/logger.ts +27 -15
- package/src/{application → core}/messages.ts +4 -4
- package/src/core/not-authorized.error.ts +11 -0
- package/src/core/problem-details.ts +57 -0
- package/src/core/rate-limit.error.ts +12 -0
- package/src/core/request.errors.ts +86 -0
- package/src/core/stale-session.error.ts +11 -0
- package/src/infrastructure/better-auth/apple-client-secret.ts +54 -0
- package/src/infrastructure/better-auth/auth-client.ts +26 -0
- package/src/infrastructure/better-auth/options.ts +232 -0
- package/src/infrastructure/better-auth/rate-limit.ts +69 -0
- package/src/infrastructure/better-auth/secondary-storage.ts +21 -0
- package/src/infrastructure/better-auth/stripe.ts +11 -0
- package/src/infrastructure/cloudflare/auth-secondary-storage.ts +113 -0
- package/src/infrastructure/cloudflare/base-d1-repository.ts +2 -2
- package/src/infrastructure/cloudflare/d1-message-store.ts +3 -7
- package/src/infrastructure/cloudflare/d1-schema.ts +2 -2
- package/src/infrastructure/postgres/base-postgres-repository.ts +2 -2
- package/src/infrastructure/postgres/postgres-message-store.ts +3 -7
- package/src/infrastructure/postgres/postgres-schema.ts +2 -2
- package/src/infrastructure/tanstack/auth.middleware.ts +89 -0
- package/src/infrastructure/tanstack/error.middleware.ts +81 -0
- package/src/infrastructure/tanstack/http-error-response.ts +30 -0
- package/src/infrastructure/tanstack/index.ts +13 -0
- package/src/infrastructure/tanstack/last-login-method.ts +13 -0
- package/src/infrastructure/tanstack/resolve-failure.ts +24 -0
- package/src/infrastructure/tanstack/session-cookies.ts +36 -0
- package/src/infrastructure/tanstack/to-error-payload.ts +32 -0
- package/src/infrastructure/unit-of-work-context.ts +3 -2
- package/dist/application-rpZoTfzU.js.map +0 -1
- package/dist/core-MjwyZJ4i.js +0 -302
- package/dist/core-MjwyZJ4i.js.map +0 -1
- package/dist/index-BN_tjxME.d.ts.map +0 -1
- package/dist/index-ToddX0m3.d.ts +0 -213
- package/dist/index-ToddX0m3.d.ts.map +0 -1
- package/dist/unit-of-work-context-BLNj72v0.js.map +0 -1
- package/dist/unit-of-work-context-CbUJGHlL.d.ts.map +0 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createMiddleware, type Register, type RequestServerOptions } from '@tanstack/react-start'
|
|
2
|
+
import { getRequestHeaders } from '@tanstack/react-start/server'
|
|
3
|
+
import { getSessionCookie } from 'better-auth/cookies'
|
|
4
|
+
|
|
5
|
+
import { AuthenticationError, StaleSessionError, createLogger } from '@/core'
|
|
6
|
+
|
|
7
|
+
const logger = createLogger('auth-middleware')
|
|
8
|
+
|
|
9
|
+
/** The fields middleware reads; generic callers retain every additional session and user field. */
|
|
10
|
+
export type AuthSession = {
|
|
11
|
+
readonly user: { readonly id: string }
|
|
12
|
+
readonly session: { readonly id: string }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Authenticated context retains the application's complete user and session types. */
|
|
16
|
+
export type RequiredAuthContext<Session extends AuthSession> = {
|
|
17
|
+
readonly user: Session['user']
|
|
18
|
+
readonly session: Session['session']
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Optional authentication supplies both fields together or neither field. */
|
|
22
|
+
export type AuthContext<Session extends AuthSession> =
|
|
23
|
+
| RequiredAuthContext<Session>
|
|
24
|
+
| {
|
|
25
|
+
readonly user: undefined
|
|
26
|
+
readonly session: undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Select the current request's Better Auth instance from TanStack's registered context. */
|
|
30
|
+
export type AuthMiddlewareOptions<Session extends AuthSession> = {
|
|
31
|
+
readonly getAuth: (context: RequestServerOptions<Register, undefined>['context']) => {
|
|
32
|
+
readonly api: {
|
|
33
|
+
readonly getSession: (options: { headers: Headers }) => Promise<Session | null>
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Preserve Porte's function and request middleware contracts with application-owned services.
|
|
40
|
+
* Optional lookup failures log once and continue as guests. Required lookup failures propagate.
|
|
41
|
+
*/
|
|
42
|
+
export function createAuthMiddleware<Session extends AuthSession>(
|
|
43
|
+
options: AuthMiddlewareOptions<Session>,
|
|
44
|
+
) {
|
|
45
|
+
const extractAuth = createMiddleware({ type: 'function' }).server<AuthContext<Session>>(
|
|
46
|
+
async ({ next, context }) => {
|
|
47
|
+
let resolved: Session | null
|
|
48
|
+
try {
|
|
49
|
+
resolved = await options.getAuth(context).api.getSession({ headers: getRequestHeaders() })
|
|
50
|
+
} catch (error) {
|
|
51
|
+
logger.error('get_session_failed', { error })
|
|
52
|
+
resolved = null
|
|
53
|
+
}
|
|
54
|
+
if (resolved === null)
|
|
55
|
+
return next<AuthContext<Session>>({ context: { user: undefined, session: undefined } })
|
|
56
|
+
return next<AuthContext<Session>>({
|
|
57
|
+
context: { user: resolved.user, session: resolved.session },
|
|
58
|
+
})
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async function resolveRequiredSession(
|
|
63
|
+
context: RequestServerOptions<Register, undefined>['context'],
|
|
64
|
+
headers: Headers,
|
|
65
|
+
): Promise<Session> {
|
|
66
|
+
const resolved = await options.getAuth(context).api.getSession({ headers })
|
|
67
|
+
if (resolved === null) {
|
|
68
|
+
if (getSessionCookie(headers)) throw new StaleSessionError()
|
|
69
|
+
throw new AuthenticationError()
|
|
70
|
+
}
|
|
71
|
+
return resolved
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const requireAuth = createMiddleware({ type: 'function' }).server(async ({ next, context }) => {
|
|
75
|
+
const resolved = await resolveRequiredSession(context, getRequestHeaders())
|
|
76
|
+
return next<RequiredAuthContext<Session>>({
|
|
77
|
+
context: { user: resolved.user, session: resolved.session },
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
const requireAuthRequest = createMiddleware({ type: 'request' }).server(
|
|
81
|
+
async ({ request, next, context }) => {
|
|
82
|
+
const resolved = await resolveRequiredSession(context, request.headers)
|
|
83
|
+
return next<RequiredAuthContext<Session>>({
|
|
84
|
+
context: { user: resolved.user, session: resolved.session },
|
|
85
|
+
})
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
return { extractAuth, requireAuth, requireAuthRequest }
|
|
89
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isNotFound, isRedirect, type AnyRedirect } from '@tanstack/react-router'
|
|
2
|
+
import { createMiddleware, type Register, type RequestServerOptions } from '@tanstack/react-start'
|
|
3
|
+
import type { BetterAuthCookies } from 'better-auth'
|
|
4
|
+
|
|
5
|
+
import { StaleSessionError, type ErrorTable } from '@/core'
|
|
6
|
+
|
|
7
|
+
import { createHttpErrorResponseConverter } from './http-error-response.ts'
|
|
8
|
+
import { clearStaleSessionCookies } from './session-cookies.ts'
|
|
9
|
+
import { createErrorPayloadConverter } from './to-error-payload.ts'
|
|
10
|
+
|
|
11
|
+
type RequestContext = RequestServerOptions<Register, undefined>['context']
|
|
12
|
+
|
|
13
|
+
/** The part of a Better Auth instance the boundary reads: its computed cookie definitions. */
|
|
14
|
+
export type AuthCookieSource = {
|
|
15
|
+
readonly $context: Promise<{ readonly authCookies: BetterAuthCookies }>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The application's error table, its stale-session redirect, and its Better Auth instance. */
|
|
19
|
+
export type ErrorMiddlewareOptions<Entries> = {
|
|
20
|
+
readonly errors: ErrorTable<Entries>
|
|
21
|
+
/** Build the redirect a server function throws for a stale session. */
|
|
22
|
+
readonly signIn: () => AnyRedirect
|
|
23
|
+
/** Select the request's Better Auth instance; its cookie definitions say what to clear. */
|
|
24
|
+
readonly getAuth: (context: RequestContext) => AuthCookieSource
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build both transport boundaries from one error table.
|
|
29
|
+
* Register `functionErrorMiddleware` globally. Put `routeErrorMiddleware` first in an API route's `server.middleware`.
|
|
30
|
+
*/
|
|
31
|
+
export function createErrorMiddleware<Entries>(options: ErrorMiddlewareOptions<Entries>) {
|
|
32
|
+
const toErrorPayload = createErrorPayloadConverter(options.errors)
|
|
33
|
+
const toHttpErrorResponse = createHttpErrorResponseConverter(options.errors)
|
|
34
|
+
|
|
35
|
+
async function clearSession(context: RequestContext): Promise<void> {
|
|
36
|
+
const { authCookies } = await options.getAuth(context).$context
|
|
37
|
+
clearStaleSessionCookies(authCookies)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function handleFunctionError(cause: unknown, context: RequestContext): Promise<never> {
|
|
41
|
+
// oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.
|
|
42
|
+
if (isNotFound(cause) || isRedirect(cause)) throw cause
|
|
43
|
+
if (cause instanceof StaleSessionError) {
|
|
44
|
+
await clearSession(context)
|
|
45
|
+
// oxlint-disable-next-line typescript/only-throw-error -- Router redirects by throwing this.
|
|
46
|
+
throw options.signIn()
|
|
47
|
+
}
|
|
48
|
+
// oxlint-disable-next-line typescript/only-throw-error -- The client rejects with this and reads its tag.
|
|
49
|
+
throw toErrorPayload(cause)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A route answers 401 instead of a redirect because fetch would follow it into the HTML shell.
|
|
53
|
+
async function handleRouteError(cause: unknown, context: RequestContext): Promise<Response> {
|
|
54
|
+
// oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.
|
|
55
|
+
if (isNotFound(cause) || isRedirect(cause)) throw cause
|
|
56
|
+
if (cause instanceof StaleSessionError) await clearSession(context)
|
|
57
|
+
return toHttpErrorResponse(cause)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const functionErrorMiddleware = createMiddleware({ type: 'function' }).server(
|
|
61
|
+
async ({ next, context }) => {
|
|
62
|
+
try {
|
|
63
|
+
return await next()
|
|
64
|
+
} catch (cause) {
|
|
65
|
+
return handleFunctionError(cause, context)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const routeErrorMiddleware = createMiddleware({ type: 'request' }).server(
|
|
71
|
+
async ({ next, context }) => {
|
|
72
|
+
try {
|
|
73
|
+
return await next()
|
|
74
|
+
} catch (cause) {
|
|
75
|
+
return handleRouteError(cause, context)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return { functionErrorMiddleware, routeErrorMiddleware, toErrorPayload, toHttpErrorResponse }
|
|
81
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getReasonPhrase } from 'http-status-codes'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import type { ErrorTable, ProblemDetails } from '@/core'
|
|
5
|
+
|
|
6
|
+
import { resolveFailure } from './resolve-failure.ts'
|
|
7
|
+
|
|
8
|
+
// A known delay becomes the header; a null delay sends no header.
|
|
9
|
+
const RetryAfterSchema = z.object({ retryAfter: z.number() })
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build the API route converter for one error table.
|
|
13
|
+
* The body is RFC 9457 problem details; public fields become extension members.
|
|
14
|
+
*/
|
|
15
|
+
export function createHttpErrorResponseConverter<Entries>(table: ErrorTable<Entries>) {
|
|
16
|
+
return function toHttpErrorResponse(cause: unknown): Response {
|
|
17
|
+
const { status, payload } = resolveFailure(table, cause)
|
|
18
|
+
const { _tag, message, ...fields } = payload
|
|
19
|
+
const body: ProblemDetails = {
|
|
20
|
+
type: 'about:blank',
|
|
21
|
+
title: getReasonPhrase(status),
|
|
22
|
+
detail: message,
|
|
23
|
+
...fields,
|
|
24
|
+
}
|
|
25
|
+
const headers = new Headers({ 'Content-Type': 'application/problem+json' })
|
|
26
|
+
const retry = RetryAfterSchema.safeParse(payload)
|
|
27
|
+
if (retry.success) headers.set('Retry-After', String(retry.data.retryAfter))
|
|
28
|
+
return Response.json(body, { status, headers })
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createAuthMiddleware,
|
|
3
|
+
type AuthSession,
|
|
4
|
+
type AuthContext,
|
|
5
|
+
type RequiredAuthContext,
|
|
6
|
+
type AuthMiddlewareOptions,
|
|
7
|
+
} from './auth.middleware.ts'
|
|
8
|
+
export {
|
|
9
|
+
createErrorMiddleware,
|
|
10
|
+
type AuthCookieSource,
|
|
11
|
+
type ErrorMiddlewareOptions,
|
|
12
|
+
} from './error.middleware.ts'
|
|
13
|
+
export { getLastUsedLoginMethod } from './last-login-method.ts'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getRequestHeaders } from '@tanstack/react-start/server'
|
|
2
|
+
import { parseCookies } from 'better-auth/cookies'
|
|
3
|
+
|
|
4
|
+
// Better Auth keeps the plugin default private; this is the value LastLoginMethodOptions.cookieName documents.
|
|
5
|
+
const DEFAULT_COOKIE_NAME = 'better-auth.last_used_login_method'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Read the method this browser last signed in with from the current request, for the sign-in loader on the server.
|
|
9
|
+
* Pass `cookieName` only when `integrations.lastLoginMethod.cookieName` is set. Returns `null` when the cookie is absent.
|
|
10
|
+
*/
|
|
11
|
+
export function getLastUsedLoginMethod(cookieName = DEFAULT_COOKIE_NAME): string | null {
|
|
12
|
+
return parseCookies(getRequestHeaders().get('cookie') ?? '').get(cookieName) ?? null
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AppError, createLogger, type ErrorTable, type ResolvedError } from '@/core'
|
|
2
|
+
import type { ErrorPayloadOf } from '@/core'
|
|
3
|
+
|
|
4
|
+
const logger = createLogger('error-boundary')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve one failure through the table or fall back to the generic replacement, and log it once.
|
|
8
|
+
* Both transport converters call this, so one rule decides the log level: mapped is a warning, unmapped is an error with the cause.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveFailure<Entries>(
|
|
11
|
+
table: ErrorTable<Entries>,
|
|
12
|
+
cause: unknown,
|
|
13
|
+
): ResolvedError<ErrorPayloadOf<ErrorTable<Entries>>> {
|
|
14
|
+
const resolved = table.resolve(cause)
|
|
15
|
+
if (resolved !== null) {
|
|
16
|
+
logger.warn('handled_error', {
|
|
17
|
+
details: { tag: resolved.payload._tag, status: resolved.status },
|
|
18
|
+
})
|
|
19
|
+
return resolved
|
|
20
|
+
}
|
|
21
|
+
const classification = cause instanceof AppError ? cause.classification : 'unknown'
|
|
22
|
+
logger.error('unexpected_error', { error: cause, details: { classification } })
|
|
23
|
+
return table.fallback(cause)
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { deleteCookie, getRequestHeaders } from '@tanstack/react-start/server'
|
|
2
|
+
import type { BetterAuthCookies } from 'better-auth'
|
|
3
|
+
import { parseCookies } from 'better-auth/cookies'
|
|
4
|
+
|
|
5
|
+
type AuthCookie = BetterAuthCookies['sessionToken']
|
|
6
|
+
type DeleteCookieOptions = NonNullable<Parameters<typeof deleteCookie>[1]>
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Delete the session, session cache, account cache, and remember-me cookies of one instance.
|
|
10
|
+
* Chunks present on the request go too. Names and attributes come from the instance, so custom prefixes and domain cookies clear.
|
|
11
|
+
*/
|
|
12
|
+
export function clearStaleSessionCookies(cookies: BetterAuthCookies): void {
|
|
13
|
+
const present = [...parseCookies(getRequestHeaders().get('cookie') ?? '').keys()]
|
|
14
|
+
const definitions = [
|
|
15
|
+
cookies.sessionToken,
|
|
16
|
+
cookies.sessionData,
|
|
17
|
+
cookies.accountData,
|
|
18
|
+
cookies.dontRememberToken,
|
|
19
|
+
]
|
|
20
|
+
for (const cookie of definitions) {
|
|
21
|
+
// Better Auth names a chunk `${name}.${index}`, so a missed chunk would resurrect the value.
|
|
22
|
+
const chunks = present.filter((name) => name.startsWith(`${cookie.name}.`))
|
|
23
|
+
for (const name of new Set([cookie.name, ...chunks]))
|
|
24
|
+
deleteCookie(name, deletionOf(cookie.attributes))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function deletionOf(attributes: AuthCookie['attributes']): DeleteCookieOptions {
|
|
29
|
+
// Better Auth sets `/` and the secure flag at runtime; the types leave both optional.
|
|
30
|
+
const options: DeleteCookieOptions = {
|
|
31
|
+
path: attributes.path ?? '/',
|
|
32
|
+
secure: attributes.secure ?? false,
|
|
33
|
+
}
|
|
34
|
+
if (attributes.domain !== undefined) options.domain = attributes.domain
|
|
35
|
+
return options
|
|
36
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { ValidationError, type ErrorPayloadOf, type ErrorTable } from '@/core'
|
|
4
|
+
|
|
5
|
+
import { resolveFailure } from './resolve-failure.ts'
|
|
6
|
+
|
|
7
|
+
// TanStack serializes a validator's issues into `Error.message` as JSON.
|
|
8
|
+
const SerializedIssuesSchema = z
|
|
9
|
+
.array(z.object({ message: z.string(), path: z.array(z.union([z.string(), z.number()])) }))
|
|
10
|
+
.min(1)
|
|
11
|
+
|
|
12
|
+
function reconstructValidationError(cause: unknown): ValidationError | null {
|
|
13
|
+
if (!(cause instanceof Error)) return null
|
|
14
|
+
let message: unknown
|
|
15
|
+
try {
|
|
16
|
+
message = JSON.parse(cause.message)
|
|
17
|
+
} catch {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
const parsed = SerializedIssuesSchema.safeParse(message)
|
|
21
|
+
return parsed.success ? ValidationError.fromIssues(parsed.data) : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the server-function converter for one error table.
|
|
26
|
+
* The result is a plain object, because only own data survives serialization.
|
|
27
|
+
*/
|
|
28
|
+
export function createErrorPayloadConverter<Entries>(table: ErrorTable<Entries>) {
|
|
29
|
+
return function toErrorPayload(cause: unknown): ErrorPayloadOf<ErrorTable<Entries>> {
|
|
30
|
+
return resolveFailure(table, reconstructValidationError(cause) ?? cause).payload
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -3,9 +3,10 @@ import { AsyncLocalStorage } from 'node:async_hooks'
|
|
|
3
3
|
import { panic } from 'better-result'
|
|
4
4
|
import type { BatchItem } from 'drizzle-orm/batch'
|
|
5
5
|
|
|
6
|
-
import { UnitOfWorkAlreadyActiveError, type
|
|
6
|
+
import { UnitOfWorkAlreadyActiveError, type IEventCollector } from '@/application'
|
|
7
|
+
import type { DomainEvent } from '@/core'
|
|
7
8
|
|
|
8
|
-
type UnitOfWorkContext = { readonly events:
|
|
9
|
+
type UnitOfWorkContext = { readonly events: DomainEvent[] } & (
|
|
9
10
|
| { readonly database: 'd1'; readonly statements: BatchItem<'sqlite'>[] }
|
|
10
11
|
| { readonly database: 'postgres' }
|
|
11
12
|
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"application-rpZoTfzU.js","names":[],"sources":["../src/application/message-already-processed.error.ts","../src/application/message-bus.ts","../src/application/unit-of-work.ts","../src/application/unit-of-work.errors.ts"],"sourcesContent":["import { AppError, DatabaseError } from '@/core'\n\nimport type { MessageId } from './messages.ts'\n\n/** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */\nexport class MessageAlreadyProcessedError extends AppError {\n readonly _tag = 'MessageAlreadyProcessedError'\n readonly classification = 'terminal'\n\n constructor(\n readonly messageId: MessageId,\n options?: { readonly cause: unknown },\n ) {\n super(`message ${messageId} already processed`, options)\n }\n\n /** Recognize only the processed-message primary key; preserve other failures unchanged at the call site. */\n static from(cause: unknown, messageId: MessageId): MessageAlreadyProcessedError | undefined {\n if (cause instanceof MessageAlreadyProcessedError) return cause\n if (!(cause instanceof DatabaseError)) return undefined\n const details = cause.details\n const duplicate =\n details.database === 'd1'\n ? details.kind === 'unique' &&\n details.table === 'processed_messages' &&\n details.column === 'message_id'\n : details.code === '23505' &&\n details.table_name === 'processed_messages' &&\n details.constraint_name === 'processed_messages_pkey'\n if (!duplicate) return undefined\n return new MessageAlreadyProcessedError(messageId, { cause })\n }\n}\n","import { panic } from 'better-result'\n\nimport { createLogger, unreachable } from '@/core'\n\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { Command, Event, Message, Query } from './messages.ts'\nimport type { Handlers, MessageResult, RegistryMessage } from './registry.ts'\n\nconst logger = createLogger('message-bus')\n\n/** The port an entrypoint dispatches through. `AppDeps` names this as `IMessageBus<typeof registry>`. */\nexport interface IMessageBus<TRegistry extends Handlers<never>> {\n handle<TMessage extends Message & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>>\n}\n\n/**\n * Routes one message to its slot. The bus opens no transaction and keeps no outbox:\n * a handler that writes opens the unit of work itself.\n */\nexport class MessageBus<Deps, TRegistry extends Handlers<Deps>> implements IMessageBus<TRegistry> {\n constructor(\n private readonly deps: Deps,\n private readonly registry: TRegistry,\n ) {}\n\n /** Resolves to the handler's own value for a command or query, and to void for an event. */\n async handle<TMessage extends Message & RegistryMessage<TRegistry>>(\n message: TMessage,\n ): Promise<MessageResult<TMessage, TRegistry>> {\n // SAFETY: the slot for this name is what `MessageResult` names, and dispatch returns its value.\n return (await this.dispatch(message)) as MessageResult<TMessage, TRegistry>\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- one table holds every slot's value; `handle` names it.\n private dispatch(message: Message): Promise<unknown> {\n if (message.type === 'command') return this.handleCommand(message)\n if (message.type === 'query') return this.handleQuery(message)\n if (message.type === 'event') return this.handleEvent(message)\n // A message parsed at a boundary cannot reach here; one that does must not resolve silently.\n return unreachable(message)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleCommand(command: Command): Promise<unknown> {\n // `satisfies` proved the registry complete, so a missing slot is a broken registry object.\n const handler = this.registry.commands[command.name]\n if (handler === undefined) return panic(`No handler for command ${command.name}`)\n logger.debug('handling_command', { name: command.name, id: command.id })\n return handler(command, this.deps)\n }\n\n // oxlint-disable-next-line anti-slop/no-unknown-returns -- see dispatch.\n private handleQuery(query: Query): Promise<unknown> {\n const handler = this.registry.queries[query.name]\n if (handler === undefined) return panic(`No handler for query ${query.name}`)\n return handler(query, this.deps)\n }\n\n /**\n * Every subscriber runs to completion. One failure is rethrown as is. Several are thrown as\n * one `AggregateError`. A `MessageAlreadyProcessedError` means that subscriber already committed on\n * an earlier delivery, so it counts as delivered.\n */\n private async handleEvent(event: Event): Promise<void> {\n const subscriptions = this.registry.events[event.name] ?? []\n const outcomes = await Promise.allSettled(\n subscriptions.map((subscription) => subscription(event, this.deps)),\n )\n const failures: unknown[] = []\n for (const [subscriber, outcome] of outcomes.entries()) {\n if (outcome.status === 'fulfilled') continue\n if (outcome.reason instanceof MessageAlreadyProcessedError) {\n // The bus swallows this error, so this line is the only record that the skip happened.\n logger.info('duplicate_subscriber_ignored', { event: event.name, id: event.id, subscriber })\n continue\n }\n failures.push(outcome.reason)\n }\n if (failures.length === 1) throw failures[0]\n if (failures.length > 1) {\n throw new AggregateError(failures, `${event.name}: ${failures.length} subscribers failed`)\n }\n }\n}\n","import type { IEventCollector } from './event-collector.ts'\nimport { MessageAlreadyProcessedError } from './message-already-processed.error.ts'\nimport type { IMessageStore } from './message-store.ts'\nimport type { MessageId } from './messages.ts'\nimport type { IOutboxRelay } from './outbox-relay.ts'\nimport type { ITransaction } from './transaction.ts'\n\n/**\n * Commits the processed-message record, repository writes, and emitted events together before resolving.\n * An existing record rejects with `MessageAlreadyProcessedError`; external effects must run through the outbox after commit.\n * Nested calls reject with `UnitOfWorkAlreadyActiveError`; independent concurrent calls are allowed.\n */\nexport interface IUnitOfWork<Repos> {\n run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>\n}\n\nexport type UnitOfWorkDeps<Tx, Repos> = {\n readonly transaction: ITransaction<Tx>\n /** The processed-messages and outbox repository for this transaction. */\n readonly messageStore: (tx: Tx) => IMessageStore\n /** Repositories participate in the current transaction. */\n readonly createRepositories: (tx: Tx) => Repos\n readonly eventCollector: IEventCollector\n readonly relay: IOutboxRelay\n}\n\n/** The unit of work: the boundary, the repositories, the events. Every database call is a port. */\nexport class UnitOfWork<Tx, Repos> implements IUnitOfWork<Repos> {\n constructor(private readonly deps: UnitOfWorkDeps<Tx, Repos>) {}\n\n async run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T> {\n const { transaction, messageStore, createRepositories, eventCollector, relay } = this.deps\n let written = 0\n\n let value: T\n try {\n value = await transaction.transaction(async (tx) => {\n const store = messageStore(tx)\n await store.claim(messageId)\n const repos = createRepositories(tx)\n const result = await work(repos)\n const pending = eventCollector.collect()\n await store.persistEvents(pending)\n written = pending.length\n return result\n })\n } catch (error) {\n throw MessageAlreadyProcessedError.from(error, messageId) ?? error\n }\n\n if (written > 0) relay.wake()\n return value\n }\n}\n","import { AppError } from '@/core'\n\n/** A boundary was opened inside another. One message, one atomic scope. */\nexport class UnitOfWorkAlreadyActiveError extends AppError {\n readonly _tag = 'UnitOfWorkAlreadyActiveError'\n readonly classification = 'terminal'\n\n constructor() {\n super('a unit of work is already active')\n }\n}\n"],"mappings":"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"}
|
package/dist/core-MjwyZJ4i.js
DELETED
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import { panic } from "better-result";
|
|
2
|
-
|
|
3
|
-
//#region src/core/app-error.ts
|
|
4
|
-
/**
|
|
5
|
-
* The base of every expected failure. A subclass declares its tag and its classification,
|
|
6
|
-
* so no boundary has to guess what kind of failure it holds. A boundary tells our errors from
|
|
7
|
-
* raw throws with `instanceof AppError`; a value that fails it was never judged.
|
|
8
|
-
*
|
|
9
|
-
* A `readonly` field with a literal initializer keeps the literal type, so
|
|
10
|
-
* `readonly _tag = 'StoreUnavailable'` is enough for `Result` unions and `matchError`.
|
|
11
|
-
*/
|
|
12
|
-
var AppError = class extends Error {
|
|
13
|
-
get name() {
|
|
14
|
-
return this._tag;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/core/database.error.ts
|
|
20
|
-
/** Classification describes the failure; callers must separately establish whether repetition is safe. */
|
|
21
|
-
var DatabaseError = class extends AppError {
|
|
22
|
-
operation;
|
|
23
|
-
classification;
|
|
24
|
-
details;
|
|
25
|
-
_tag = "DatabaseError";
|
|
26
|
-
constructor(operation, classification, details, options) {
|
|
27
|
-
super(`Database operation failed: ${operation}`, options);
|
|
28
|
-
this.operation = operation;
|
|
29
|
-
this.classification = classification;
|
|
30
|
-
this.details = details;
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
//#endregion
|
|
35
|
-
//#region src/core/defects.ts
|
|
36
|
-
/**
|
|
37
|
-
* Mark a line the types say cannot run. A union member without a branch stops compilation here.
|
|
38
|
-
*
|
|
39
|
-
* @throws Panic when a value outside the union arrives at runtime.
|
|
40
|
-
*/
|
|
41
|
-
function unreachable(value) {
|
|
42
|
-
return panic(`Unreachable: ${String(value)}`);
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Mark a body that is not written yet. A defect, not an expected failure.
|
|
46
|
-
*
|
|
47
|
-
* @param what - The behavior the body will provide, such as `invoice export`.
|
|
48
|
-
* @throws Panic always.
|
|
49
|
-
*/
|
|
50
|
-
function notImplemented(what) {
|
|
51
|
-
return panic(`Not implemented: ${what}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
//#endregion
|
|
55
|
-
//#region src/core/failure-classification.ts
|
|
56
|
-
/** Decide whether one retry owner may repeat a failed operation. */
|
|
57
|
-
function shouldRetryFailure(args) {
|
|
58
|
-
if (!args.repeatSafe) return false;
|
|
59
|
-
if (args.classification === "transient") return true;
|
|
60
|
-
if (args.classification === "terminal") return false;
|
|
61
|
-
return args.owner === "durable";
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
//#endregion
|
|
65
|
-
//#region src/core/transport-classification.ts
|
|
66
|
-
/**
|
|
67
|
-
* Messages that the platform-neutral transports raise for a failure that may pass.
|
|
68
|
-
*
|
|
69
|
-
* A boundary that knows its own runtime adds its own patterns before these.
|
|
70
|
-
*/
|
|
71
|
-
const TRANSIENT_TRANSPORT_PATTERNS = [
|
|
72
|
-
/network/,
|
|
73
|
-
/fetch failed/,
|
|
74
|
-
/timeout/,
|
|
75
|
-
/timed?\s*out/,
|
|
76
|
-
/connection.*(lost|reset|refused|closed|aborted)/,
|
|
77
|
-
/econnreset/,
|
|
78
|
-
/econnrefused/,
|
|
79
|
-
/etimedout/,
|
|
80
|
-
/eai_again/
|
|
81
|
-
];
|
|
82
|
-
/** Cancellation is a decision, not a failure to repeat. */
|
|
83
|
-
function isAbortError(cause) {
|
|
84
|
-
return cause instanceof Error && cause.name === "AbortError";
|
|
85
|
-
}
|
|
86
|
-
/** Match known transient transport messages while excluding explicit cancellation. */
|
|
87
|
-
function isTransientTransportError(cause) {
|
|
88
|
-
if (!(cause instanceof Error) || isAbortError(cause)) return false;
|
|
89
|
-
const message = cause.message.toLowerCase();
|
|
90
|
-
return TRANSIENT_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/core/logger.ts
|
|
95
|
-
/**
|
|
96
|
-
* The supported log severity levels, in ascending order.
|
|
97
|
-
*
|
|
98
|
-
* A const object, not an enum, so a parsed environment value typed `'INFO'` is a `LogLevel`.
|
|
99
|
-
*/
|
|
100
|
-
const LogLevel = {
|
|
101
|
-
DEBUG: "DEBUG",
|
|
102
|
-
INFO: "INFO",
|
|
103
|
-
WARN: "WARN",
|
|
104
|
-
ERROR: "ERROR"
|
|
105
|
-
};
|
|
106
|
-
const DEFAULT_SETTINGS = {
|
|
107
|
-
level: LogLevel.INFO,
|
|
108
|
-
format: "json"
|
|
109
|
-
};
|
|
110
|
-
let settings = DEFAULT_SETTINGS;
|
|
111
|
-
/**
|
|
112
|
-
* Set application settings, including for existing loggers. Per-logger overrides take priority.
|
|
113
|
-
* Call at startup, never per request. An omitted field resets to INFO and JSON output.
|
|
114
|
-
*/
|
|
115
|
-
function configureLogger(config) {
|
|
116
|
-
settings = {
|
|
117
|
-
level: config.level ?? DEFAULT_SETTINGS.level,
|
|
118
|
-
format: config.format ?? DEFAULT_SETTINGS.format
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
let errorHook = null;
|
|
122
|
-
/**
|
|
123
|
-
* Register the function that receives each logged error.
|
|
124
|
-
*
|
|
125
|
-
* Set once at the process entry point. A later call replaces the earlier hook.
|
|
126
|
-
*/
|
|
127
|
-
function setLoggerErrorHook(fn) {
|
|
128
|
-
errorHook = fn;
|
|
129
|
-
}
|
|
130
|
-
const CONSOLE_METHOD = {
|
|
131
|
-
[LogLevel.DEBUG]: "debug",
|
|
132
|
-
[LogLevel.INFO]: "info",
|
|
133
|
-
[LogLevel.WARN]: "warn",
|
|
134
|
-
[LogLevel.ERROR]: "error"
|
|
135
|
-
};
|
|
136
|
-
const ESC = String.fromCharCode(27);
|
|
137
|
-
const ANSI_RESET = `${ESC}[0m`;
|
|
138
|
-
const ANSI_GRAY = `${ESC}[90m`;
|
|
139
|
-
const ANSI_BLUE = `${ESC}[34m`;
|
|
140
|
-
const LEVEL_ANSI = {
|
|
141
|
-
DEBUG: `${ESC}[2;36m`,
|
|
142
|
-
INFO: ANSI_BLUE,
|
|
143
|
-
WARN: `${ESC}[33m`,
|
|
144
|
-
ERROR: `${ESC}[1;31m`
|
|
145
|
-
};
|
|
146
|
-
function prettyLine(entry) {
|
|
147
|
-
const level = `[${entry.level}]`;
|
|
148
|
-
const module = `[${entry.module}]`;
|
|
149
|
-
if ("window" in globalThis) return `${entry.timestamp} ${level} ${module} ${entry.message}`;
|
|
150
|
-
return `${`${ANSI_GRAY}${entry.timestamp}${ANSI_RESET}`} ${`${LEVEL_ANSI[entry.level]}${level}${ANSI_RESET}`} ${`${ANSI_BLUE}${module}${ANSI_RESET}`} ${entry.message}`;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* JSON format writes the entry object. Pretty format writes one line, then the data,
|
|
154
|
-
* the error, and the stack as separate console arguments.
|
|
155
|
-
*/
|
|
156
|
-
const consoleSink = (entry) => {
|
|
157
|
-
const write = console[CONSOLE_METHOD[entry.level]];
|
|
158
|
-
if (settings.format === "json") {
|
|
159
|
-
write(entry);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const line = prettyLine(entry);
|
|
163
|
-
if (entry.error === void 0) {
|
|
164
|
-
write(line, ...entry.data);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const { stack, ...error } = entry.error;
|
|
168
|
-
if (stack === void 0) write(line, ...entry.data, error);
|
|
169
|
-
else write(line, ...entry.data, error, `\n${stack}`);
|
|
170
|
-
};
|
|
171
|
-
let logSink = consoleSink;
|
|
172
|
-
/**
|
|
173
|
-
* Send every log entry somewhere other than `console`.
|
|
174
|
-
*
|
|
175
|
-
* Set once at the process entry point, never per module. A logger is created
|
|
176
|
-
* by name and nothing else, so its destination is a fact about the process.
|
|
177
|
-
*/
|
|
178
|
-
function setLogSink(fn) {
|
|
179
|
-
logSink = fn;
|
|
180
|
-
}
|
|
181
|
-
const LOG_LEVELS = Object.values(LogLevel);
|
|
182
|
-
const MAX_ERROR_DEPTH = 3;
|
|
183
|
-
/**
|
|
184
|
-
* Diagnostics worth keeping off an error, named one by one.
|
|
185
|
-
*
|
|
186
|
-
* An allowlist rather than every own property: an error raised by a library we
|
|
187
|
-
* do not control may hang a request or a user payload off itself, and a log is
|
|
188
|
-
* the wrong place to discover that. `_tag` and `classification` are what every `AppError`
|
|
189
|
-
* declares. `remote` and the three flags after it are the ones workerd sets itself.
|
|
190
|
-
*/
|
|
191
|
-
const KEPT_ERROR_FIELDS = [
|
|
192
|
-
"_tag",
|
|
193
|
-
"classification",
|
|
194
|
-
"code",
|
|
195
|
-
"operation",
|
|
196
|
-
"status",
|
|
197
|
-
"statusCode",
|
|
198
|
-
"remote",
|
|
199
|
-
"retryable",
|
|
200
|
-
"overloaded",
|
|
201
|
-
"durableObjectReset"
|
|
202
|
-
];
|
|
203
|
-
const serializeError = (error, depth = 0) => {
|
|
204
|
-
if (depth > MAX_ERROR_DEPTH) return {
|
|
205
|
-
name: "Error",
|
|
206
|
-
message: "[error nesting truncated]"
|
|
207
|
-
};
|
|
208
|
-
const carrier = error;
|
|
209
|
-
const kept = KEPT_ERROR_FIELDS.filter((key) => carrier[key] !== void 0);
|
|
210
|
-
const serialized = {
|
|
211
|
-
name: error.name,
|
|
212
|
-
message: error.message
|
|
213
|
-
};
|
|
214
|
-
if (error.stack !== void 0) serialized.stack = error.stack;
|
|
215
|
-
if (kept.length > 0) serialized.fields = Object.fromEntries(kept.map((key) => [key, carrier[key]]));
|
|
216
|
-
if (error.cause instanceof Error) serialized.cause = serializeError(error.cause, depth + 1);
|
|
217
|
-
else if (error.cause !== void 0) serialized.cause = {
|
|
218
|
-
name: "Error",
|
|
219
|
-
message: "[non-error cause]"
|
|
220
|
-
};
|
|
221
|
-
if (error instanceof AggregateError) serialized.errors = error.errors.map((failure) => failure instanceof Error ? serializeError(failure, depth + 1) : {
|
|
222
|
-
name: "NonError",
|
|
223
|
-
message: "[non-error aggregate member]"
|
|
224
|
-
});
|
|
225
|
-
return serialized;
|
|
226
|
-
};
|
|
227
|
-
/** Write module-scoped logs with application defaults and optional overrides. */
|
|
228
|
-
var Logger = class {
|
|
229
|
-
module;
|
|
230
|
-
config;
|
|
231
|
-
/** Create a logger for one module. */
|
|
232
|
-
constructor(module, overrideConfig) {
|
|
233
|
-
this.module = module;
|
|
234
|
-
this.config = { ...overrideConfig };
|
|
235
|
-
}
|
|
236
|
-
log(level, message, data, error) {
|
|
237
|
-
const threshold = this.config.level ?? settings.level;
|
|
238
|
-
if (threshold === "OFF") return;
|
|
239
|
-
if (LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf(threshold)) return;
|
|
240
|
-
const entry = {
|
|
241
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
242
|
-
level,
|
|
243
|
-
module: this.module,
|
|
244
|
-
message,
|
|
245
|
-
data
|
|
246
|
-
};
|
|
247
|
-
logSink(error === void 0 ? entry : {
|
|
248
|
-
...entry,
|
|
249
|
-
error
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
/** Write a debug log. */
|
|
253
|
-
debug(message, ...values) {
|
|
254
|
-
this.log(LogLevel.DEBUG, message, values);
|
|
255
|
-
}
|
|
256
|
-
/** Write an information log. */
|
|
257
|
-
info(message, ...values) {
|
|
258
|
-
this.log(LogLevel.INFO, message, values);
|
|
259
|
-
}
|
|
260
|
-
/** Write a warning log. */
|
|
261
|
-
warn(message, ...values) {
|
|
262
|
-
this.log(LogLevel.WARN, message, values);
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Write an error log and send the error to the configured hook.
|
|
266
|
-
*
|
|
267
|
-
* The details say which call this was, and the error says what went wrong
|
|
268
|
-
* inside it. The entry carries the details as data and the error beside them.
|
|
269
|
-
*/
|
|
270
|
-
error(message, context = {}) {
|
|
271
|
-
const details = context.details ?? {};
|
|
272
|
-
const thrown = context.error;
|
|
273
|
-
let error;
|
|
274
|
-
if (thrown instanceof Error) error = serializeError(thrown);
|
|
275
|
-
else if (thrown !== void 0) {
|
|
276
|
-
let json;
|
|
277
|
-
try {
|
|
278
|
-
json = JSON.stringify(thrown);
|
|
279
|
-
} catch {
|
|
280
|
-
json = "[non-serializable error]";
|
|
281
|
-
}
|
|
282
|
-
error = {
|
|
283
|
-
name: "NonError",
|
|
284
|
-
message: json
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
this.log(LogLevel.ERROR, message, context.details === void 0 ? [] : [details], error);
|
|
288
|
-
if (errorHook !== null && thrown !== void 0) errorHook({
|
|
289
|
-
error: thrown,
|
|
290
|
-
distinctId: context.userId,
|
|
291
|
-
context: details
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
/** Create a logger for one module. */
|
|
296
|
-
function createLogger(module, config) {
|
|
297
|
-
return new Logger(module, config);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
//#endregion
|
|
301
|
-
export { createLogger as a, isAbortError as c, notImplemented as d, unreachable as f, consoleSink as i, isTransientTransportError as l, AppError as m, Logger as n, setLogSink as o, DatabaseError as p, configureLogger as r, setLoggerErrorHook as s, LogLevel as t, shouldRetryFailure as u };
|
|
302
|
-
//# sourceMappingURL=core-MjwyZJ4i.js.map
|