@warlock.js/core 5.5.0 → 5.7.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/CHANGELOG.md +32 -0
- package/esm/application/app.d.mts.map +1 -1
- package/esm/application/app.mjs +7 -1
- package/esm/application/app.mjs.map +1 -1
- package/esm/benchmark/profiler.d.mts.map +1 -1
- package/esm/benchmark/profiler.mjs +4 -3
- package/esm/benchmark/profiler.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +10 -7
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/cli-commands.utils.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/release-hygiene.check.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
- package/esm/cli/commands/routes/format-routes-table.mjs +3 -3
- package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
- package/esm/cli/commands/routes/route-row.mjs.map +1 -1
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/cli/parse-cli-args.mjs +1 -0
- package/esm/cli/parse-cli-args.mjs.map +1 -1
- package/esm/cli/string-similarity.mjs +13 -4
- package/esm/cli/string-similarity.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs +5 -2
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/config/config-loader.mjs +3 -1
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +11 -1
- package/esm/config/locale-configuration.mjs.map +1 -1
- package/esm/container/container-instance-registry.mjs +40 -0
- package/esm/container/container-instance-registry.mjs.map +1 -0
- package/esm/container/index.d.mts +11 -0
- package/esm/container/index.d.mts.map +1 -1
- package/esm/container/index.mjs +11 -0
- package/esm/container/index.mjs.map +1 -1
- package/esm/database/drop-tables-action.mjs.map +1 -1
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/dependency-graph.d.mts.map +1 -1
- package/esm/dev-server/dependency-graph.mjs +2 -1
- package/esm/dev-server/dependency-graph.mjs.map +1 -1
- package/esm/dev-server/dev-logger.mjs +7 -5
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +1 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/loader/build-transpile-init.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/loader/resolve-hook.mjs.map +1 -1
- package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/parse-imports.mjs +22 -8
- package/esm/dev-server/parse-imports.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/dev-server/types.d.mts.map +1 -1
- package/esm/dev-server/utils.mjs.map +1 -1
- package/esm/encryption/encrypt.mjs +1 -0
- package/esm/encryption/encrypt.mjs.map +1 -1
- package/esm/errors/container-key-missing-error.mjs +28 -0
- package/esm/errors/container-key-missing-error.mjs.map +1 -0
- package/esm/errors/unknown-locale-error.mjs +28 -0
- package/esm/errors/unknown-locale-error.mjs.map +1 -0
- package/esm/generations/add-command.action.mjs +8 -2
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
- package/esm/generations/features/ai-google.feature.mjs.map +1 -1
- package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
- package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
- package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
- package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
- package/esm/generations/features/ai.feature.mjs.map +1 -1
- package/esm/generations/features/herald.feature.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/scheduler.feature.mjs.map +1 -1
- package/esm/generations/features/shared/patch-tsconfig-include.mjs.map +1 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs +6 -1
- package/esm/generations/features/shared/relocate-conflicting-home-route.mjs.map +1 -1
- package/esm/generations/features/tailwind.feature.mjs.map +1 -1
- package/esm/generations/features/types.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/context/request-memo.mjs.map +1 -1
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/idempotency.middleware.d.mts.map +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -2
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
- package/esm/http/middleware/utils/cidr-match.mjs +1 -0
- package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
- package/esm/http/port-preflight.d.mts.map +1 -1
- package/esm/http/port-preflight.mjs.map +1 -1
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +16 -9
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +38 -0
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +47 -0
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs.map +1 -1
- package/esm/http/types.d.mts.map +1 -1
- package/esm/image/image.d.mts.map +1 -1
- package/esm/image/image.mjs.map +1 -1
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/bundle-framework-dependencies.mjs +152 -0
- package/esm/production/bundle-framework-dependencies.mjs.map +1 -0
- package/esm/production/production-builder.mjs +30 -3
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/production/tsconfig-path-aliases.mjs +99 -0
- package/esm/production/tsconfig-path-aliases.mjs.map +1 -0
- package/esm/react/index.d.mts.map +1 -1
- package/esm/react/index.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +4 -2
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/repositories/contracts/query-builder.contract.d.mts.map +1 -1
- package/esm/repositories/contracts/types.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.d.mts.map +1 -1
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/types.d.mts.map +1 -1
- package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
- package/esm/router/positional-handler-diagnostics.mjs +9 -3
- package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
- package/esm/router/route-registry.mjs +1 -1
- package/esm/router/route-registry.mjs.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +1 -2
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
- package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
- package/esm/storage/scoped-storage.d.mts.map +1 -1
- package/esm/storage/scoped-storage.mjs +1 -0
- package/esm/storage/scoped-storage.mjs.map +1 -1
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +6 -2
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/types.d.mts.map +1 -1
- package/esm/storage/utils/contain-path.mjs.map +1 -1
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +6 -1
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs.map +1 -1
- package/esm/tests/test-setup-timeout.mjs.map +1 -1
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/use-cases/use-cases-registry.d.mts.map +1 -1
- package/esm/use-cases/use-cases-registry.mjs.map +1 -1
- package/esm/utils/app-log.mjs.map +1 -1
- package/esm/utils/queue.mjs.map +1 -1
- package/esm/utils/sleep.mjs.map +1 -1
- package/esm/utils/version-compare.d.mts.map +1 -1
- package/esm/utils/version-compare.mjs +12 -4
- package/esm/utils/version-compare.mjs.map +1 -1
- package/esm/validation/plugins/database-plugin.mjs.map +1 -1
- package/esm/vite/lower-stage3-decorators.mjs +1 -0
- package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
- package/package.json +12 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response.mjs","names":[],"sources":["../../../../../../../core/src/http/response.ts"],"sourcesContent":["import type { CookieSerializeOptions } from \"@fastify/cookie\";\r\nimport config from \"@mongez/config\";\r\nimport type { EventSubscription } from \"@mongez/events\";\r\nimport events from \"@mongez/events\";\r\nimport { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { isIterable, isPlainObject, isScalar } from \"@mongez/supportive-is\";\r\nimport type { LogLevel } from \"@warlock.js/logger\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { ValidationResult } from \"@warlock.js/seal\";\r\nimport type { FastifyReply } from \"fastify\";\r\nimport fs from \"fs\";\r\nimport mime from \"mime\";\r\nimport path from \"path\";\r\nimport type React from \"react\";\r\nimport { type ReactNode } from \"react\";\r\nimport { Application } from \"../application/application\";\r\nimport type { Route } from \"../router\";\r\nimport { StorageFile } from \"../storage\";\r\nimport { renderReact } from \"./../react\";\r\nimport type { Request } from \"./request\";\r\nimport type { ResponseEvent, ResponseSSEController, ResponseStreamController } from \"./types\";\r\n\r\ntype CookieValue = string | number | boolean | Record<string, any> | Array<any>;\r\n\r\n/**\r\n * Cookie options accepted by `response.cookie()`.\r\n *\r\n * Extends Fastify's `CookieSerializeOptions` with `raw` — set to `true` to\r\n * skip the default `JSON.stringify` of the value and write it as-is. Use for\r\n * plain-string cookies (session tokens, opaque IDs) that shouldn't be JSON-quoted.\r\n *\r\n * When `raw: true`, non-string values are coerced via `String(value)`. The\r\n * read side (`request.cookie(name)`) tries `JSON.parse` first and falls back\r\n * to the raw string on parse failure, so round-tripping a raw string cookie\r\n * Just Works.\r\n */\r\nexport type CookieOptions = CookieSerializeOptions & {\r\n /**\r\n * Skip JSON.stringify and write the value as-is.\r\n *\r\n * @default false\r\n */\r\n raw?: boolean;\r\n};\r\n\r\nexport enum ResponseStatus {\r\n OK = 200,\r\n CREATED = 201,\r\n ACCEPTED = 202,\r\n MOVED_PERMANENTLY = 301,\r\n FOUND = 302,\r\n SEE_OTHER = 303,\r\n NOT_MODIFIED = 304,\r\n TEMPORARY_REDIRECT = 307,\r\n PERMANENT_REDIRECT = 308,\r\n NO_CONTENT = 204,\r\n BAD_REQUEST = 400,\r\n UNAUTHORIZED = 401,\r\n FORBIDDEN = 403,\r\n NOT_FOUND = 404,\r\n METHOD_NOT_ALLOWED = 405,\r\n CONFLICT = 409,\r\n TOO_MANY_REQUESTS = 429,\r\n INTERNAL_SERVER_ERROR = 500,\r\n SERVICE_UNAVAILABLE = 503,\r\n}\r\n\r\n/**\r\n * Options for sending files\r\n */\r\nexport type SendFileOptions = {\r\n cacheTime?: number;\r\n immutable?: boolean;\r\n inline?: boolean;\r\n filename?: string;\r\n};\r\n\r\n/**\r\n * Options for sending buffers\r\n */\r\nexport type SendBufferOptions = SendFileOptions & {\r\n contentType?: string;\r\n etag?: string;\r\n};\r\n\r\n/**\r\n * The cookie flags every response cookie gets unless something overrides them.\r\n *\r\n * Computed per call rather than hoisted to a constant because `secure` depends\r\n * on the environment, which is not known at module-evaluation time.\r\n *\r\n * `secure` is relaxed in development only: a `Secure` cookie is dropped by the\r\n * browser over plain http, which would silently break every local login. It\r\n * stays on everywhere else, including test and staging.\r\n */\r\nfunction secureCookieDefaults(): CookieSerializeOptions {\r\n return {\r\n httpOnly: true,\r\n sameSite: \"lax\",\r\n secure: !Application.isDevelopment,\r\n };\r\n}\r\n\r\nexport class Response {\r\n /**\r\n * Current route\r\n */\r\n protected route!: Route;\r\n\r\n /**\r\n * Underlying Fastify reply — a public escape hatch to capabilities the\r\n * framework's high-level helpers don't yet cover.\r\n *\r\n * **Prefer framework methods first**: `response.send()`, `response.header()`,\r\n * `response.cookie()`, `response.sendFile()`, `response.stream()`, etc.\r\n * They wire status codes, content-type detection, the event lifecycle, and\r\n * the cache-pattern replay path correctly.\r\n *\r\n * **Reach for `baseResponse` only** when the framework genuinely lacks a\r\n * helper for what you need — and when you do, file an issue so we can add\r\n * it. Streaming and SSE are the precedent here: they bypass `send()`\r\n * deliberately because the framework didn't ship chunked-write support\r\n * natively at the time. Reaching here for non-streaming work means a\r\n * missing helper, not an answer.\r\n */\r\n public baseResponse!: FastifyReply;\r\n\r\n /**\r\n * Current status code\r\n */\r\n protected currentStatusCode = 200;\r\n\r\n /**\r\n * Current response body\r\n */\r\n protected currentBody: any;\r\n\r\n /**\r\n * Request object\r\n */\r\n public request!: Request;\r\n\r\n /**\r\n * Internal events related to this particular response object\r\n */\r\n protected events = new Map<string, any[]>();\r\n\r\n /**\r\n * Parsed body\r\n * This will return the parsed body of the response\r\n * Please note that if this property is called before the response is sent, it will return undefined\r\n */\r\n public parsedBody: any;\r\n\r\n /**\r\n * Get raw response\r\n */\r\n public get raw() {\r\n return this.baseResponse.raw;\r\n }\r\n\r\n /**\r\n * Get Current response body\r\n */\r\n public get body() {\r\n return this.currentBody;\r\n }\r\n\r\n /**\r\n * Set response body\r\n */\r\n public set body(body: any) {\r\n this.currentBody = body;\r\n }\r\n\r\n /**\r\n * Add event on sending response\r\n */\r\n public onSending(callback: any) {\r\n this.events.set(\"sending\", [...(this.events.get(\"sending\") || []), callback]);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Add event on sent response\r\n */\r\n public onSent(callback: any) {\r\n this.events.set(\"sent\", [...(this.events.get(\"sent\") || []), callback]);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the Fastify response object\r\n */\r\n public setResponse(response: FastifyReply) {\r\n this.baseResponse = response;\r\n\r\n // Listen to the 'finish' event to track when response is fully sent\r\n // This works for all response types: JSON, streams, buffers, files, etc.\r\n this.baseResponse.raw.once(\"finish\", () => {\r\n this.request.endTime = Date.now();\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Reset the response state\r\n */\r\n public reset() {\r\n this.route = {} as Route;\r\n this.currentBody = null;\r\n this.currentStatusCode = 200;\r\n }\r\n\r\n /**\r\n * Set current route\r\n */\r\n public setRoute(route: Route) {\r\n this.route = route;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the content type\r\n */\r\n public get contentType() {\r\n return this.baseResponse.getHeader(\"Content-Type\");\r\n }\r\n\r\n /**\r\n * Set the content type\r\n */\r\n public setContentType(contentType: string) {\r\n this.baseResponse.header(\"Content-Type\", contentType);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the status code\r\n */\r\n public get statusCode(): number {\r\n return this.currentStatusCode ?? this.baseResponse.statusCode;\r\n }\r\n\r\n /**\r\n * Check if response status is ok\r\n */\r\n public get isOk() {\r\n return this.currentStatusCode >= 200 && this.currentStatusCode < 300;\r\n }\r\n\r\n /**\r\n * Check if the response has been sent\r\n */\r\n public get sent() {\r\n return this.baseResponse.sent;\r\n }\r\n\r\n /**\r\n * Add a listener to the response event\r\n */\r\n public static on(\r\n event: ResponseEvent,\r\n listener: (response: Response) => void,\r\n ): EventSubscription {\r\n return events.subscribe(`response.${event}`, listener);\r\n }\r\n\r\n /**\r\n * Trigger the response event\r\n */\r\n protected static async trigger(event: ResponseEvent, ...args: any[]) {\r\n // make a timeout to make sure the request events is executed first\r\n return new Promise((resolve) => {\r\n setTimeout(async () => {\r\n await events.triggerAllAsync(`response.${event}`, ...args);\r\n resolve(true);\r\n }, 0);\r\n });\r\n }\r\n\r\n /**\r\n * Parse body\r\n */\r\n protected async parseBody() {\r\n return await this.parse(this.currentBody);\r\n }\r\n\r\n /**\r\n * Parse the given value\r\n */\r\n public async parse(value: any): Promise<any> {\r\n // if it is a falsy value, return it\r\n if (!value || isScalar(value)) return value;\r\n\r\n // if it has a `toJSON` method, call it and await the result then return it\r\n if (value.toJSON) {\r\n value.request = this.request;\r\n return await value.toJSON();\r\n }\r\n\r\n // if it is iterable, an array or array-like object then parse each item\r\n if (isIterable(value)) {\r\n const values = Array.from(value);\r\n\r\n return Promise.all(\r\n values.map(async (item: any) => {\r\n return await this.parse(item);\r\n }),\r\n );\r\n }\r\n\r\n // if not plain object, then return it\r\n if (!isPlainObject(value)) {\r\n return value;\r\n }\r\n\r\n // loop over the object and check if the value and call `parse` on it\r\n for (const key in value) {\r\n const subValue = value[key];\r\n\r\n value[key] = await this.parse(subValue);\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * Make a log message\r\n */\r\n public log(message: string, level: LogLevel = \"info\") {\r\n if (!config.get(\"http.log\")) return;\r\n\r\n log.log({\r\n module: \"response\",\r\n action: this.route.method + \" \" + this.route.path.replace(\"/*\", \"\") + `:${this.request.id}`,\r\n message,\r\n type: level,\r\n context: {\r\n request: this.request,\r\n response: this,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Check if returning response is json\r\n */\r\n public get isJson() {\r\n return this.getHeader(\"Content-Type\") === \"application/json\";\r\n }\r\n\r\n /**\r\n * Send the response\r\n * @param data - Response data\r\n * @param statusCode - HTTP status code\r\n * @param triggerEvents - Whether to trigger response events (default: true)\r\n */\r\n public async send(data?: any, statusCode?: number, triggerEvents = true): Promise<Response> {\r\n // Defensive guard against double-send. The underlying Fastify reply silently\r\n // ignores subsequent sends once `sent === true`, which has historically hidden\r\n // middleware bugs (cache-pattern replay paths that returned `baseResponse.send`\r\n // ended up re-entering `Response.send` with the FastifyReply as the body).\r\n // Surfacing the misuse via `error`-level log makes the bug loud without\r\n // crashing production traffic.\r\n if (this.baseResponse.sent) {\r\n log.error(\r\n \"response\",\r\n \"send\",\r\n `send() called on already-sent response (request:${this.request?.id ?? \"unknown\"}) — likely a middleware bug`,\r\n );\r\n\r\n return this;\r\n }\r\n\r\n if (statusCode) {\r\n this.currentStatusCode = statusCode;\r\n }\r\n\r\n if (data === this) return this;\r\n\r\n if (data) {\r\n this.currentBody = data;\r\n }\r\n\r\n if (!this.currentStatusCode) {\r\n this.currentStatusCode = 200;\r\n }\r\n\r\n this.log(\"Sending response\");\r\n // Auto-pick `application/json` only when no content-type was set by the caller.\r\n // This preserves explicit overrides (e.g. `application/vnd.api+json` from a\r\n // cache replay, `application/problem+json` from an RFC 7807 error response)\r\n // while keeping the convenience default for the common object-body path.\r\n if (Array.isArray(this.currentBody) || isPlainObject(this.currentBody)) {\r\n if (!this.baseResponse.getHeader(\"Content-Type\")) {\r\n this.setContentType(\"application/json\");\r\n }\r\n }\r\n\r\n if (triggerEvents) {\r\n await Response.trigger(\"sending\", this);\r\n\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n await callback(this);\r\n }\r\n\r\n if (this.isJson) {\r\n await Response.trigger(\"sendingJson\", this);\r\n for (const callback of this.events.get(\"sendingJson\") || []) {\r\n await callback(this);\r\n }\r\n\r\n if (this.isOk) {\r\n await Response.trigger(\"sendingSuccessJson\", this);\r\n for (const callback of this.events.get(\"sendingSuccessJson\") || []) {\r\n await callback(this);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // parse the body and make sure it is transformed to sync data instead of async data\r\n if (typeof this.currentBody !== \"string\") {\r\n this.parsedBody = await this.parseBody();\r\n } else {\r\n this.parsedBody = data;\r\n }\r\n\r\n // Set the status first\r\n this.baseResponse.status(this.currentStatusCode);\r\n\r\n // Then send the response with the parsed body\r\n await this.baseResponse.send(this.parsedBody);\r\n\r\n this.log(\"Response sent\");\r\n\r\n if (triggerEvents) {\r\n // trigger the sent event\r\n Response.trigger(\"sent\", this);\r\n\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // trigger the success event if the status code is 2xx\r\n if (this.currentStatusCode >= 200 && this.currentStatusCode < 300) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n // trigger the successCreate event if the status code is 201\r\n if (this.currentStatusCode === 201) {\r\n Response.trigger(\"successCreate\", this);\r\n }\r\n\r\n // trigger the badRequest event if the status code is 400\r\n if (this.currentStatusCode === 400) {\r\n Response.trigger(\"badRequest\", this);\r\n }\r\n\r\n // trigger the unauthorized event if the status code is 401\r\n if (this.currentStatusCode === 401) {\r\n Response.trigger(\"unauthorized\", this);\r\n }\r\n\r\n // trigger the forbidden event if the status code is 403\r\n if (this.currentStatusCode === 403) {\r\n Response.trigger(\"forbidden\", this);\r\n }\r\n\r\n // trigger the notFound event if the status code is 404\r\n if (this.currentStatusCode === 404) {\r\n Response.trigger(\"notFound\", this);\r\n }\r\n\r\n // trigger the content too large event if the status code is 413\r\n if (this.currentStatusCode === 413) {\r\n Response.trigger(\"contentTooLarge\", this);\r\n }\r\n\r\n // trigger the throttled event if the status code is 429\r\n if (this.currentStatusCode === 429) {\r\n Response.trigger(\"throttled\", this);\r\n }\r\n\r\n // trigger the serverError event if the status code is 500\r\n if (this.currentStatusCode === 500) {\r\n Response.trigger(\"serverError\", this);\r\n }\r\n\r\n // trigger the error event if the status code is 4xx or 5xx\r\n if (this.currentStatusCode >= 400) {\r\n Response.trigger(\"error\", this);\r\n }\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Replay a previously-captured response shape — used by cache-pattern\r\n * middlewares (idempotency, response cache) to send a cached response\r\n * without re-running the controller.\r\n *\r\n * Preserves the cached status code, content-type, and any extra headers,\r\n * then sends the body through the standard `send()` pipeline so the full\r\n * event lifecycle still fires (`sent`, `success`, status-specific events).\r\n * That keeps cross-cutting observers (logger, metrics, audit) consistent\r\n * between fresh and replayed responses.\r\n *\r\n * @example\r\n * // Inside a cache-pattern middleware on HIT:\r\n * return response.header(\"X-Cache\", \"HIT\").replay({\r\n * status: cached.status,\r\n * body: cached.body,\r\n * contentType: cached.contentType,\r\n * });\r\n */\r\n public replay(cached: {\r\n status: number;\r\n body: unknown;\r\n contentType?: string;\r\n headers?: Record<string, string>;\r\n }): Promise<Response> {\r\n this.setStatusCode(cached.status);\r\n\r\n if (cached.contentType) {\r\n this.setContentType(cached.contentType);\r\n }\r\n\r\n if (cached.headers) {\r\n for (const [name, value] of Object.entries(cached.headers)) {\r\n this.header(name, value);\r\n }\r\n }\r\n\r\n return this.send(cached.body);\r\n }\r\n\r\n /**\r\n * Send html response\r\n */\r\n public html(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/html\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Render the given react component\r\n */\r\n public render(element: React.ReactElement | React.ComponentType, status = 200) {\r\n return this.setStatusCode(status).html(renderReact(element));\r\n }\r\n\r\n /**\r\n * Send xml response\r\n */\r\n public xml(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/xml\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Send plain text response\r\n */\r\n public text(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/plain\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Create a streaming response for progressive/chunked data sending\r\n *\r\n * This method allows you to send data in chunks and control when the response ends.\r\n * Perfect for Server-Sent Events (SSE), progressive rendering, or streaming large responses.\r\n *\r\n * @example\r\n * ```ts\r\n * const stream = response.stream(\"text/html\");\r\n * stream.send(\"<html><body>\");\r\n * stream.send(\"<h1>Hello</h1>\");\r\n * stream.render(<MyComponent />);\r\n * stream.send(\"</body></html>\");\r\n * stream.end();\r\n * ```\r\n *\r\n * @param contentType - The content type for the stream (default: \"text/plain\")\r\n * @returns Stream controller with send(), render(), and end() methods\r\n */\r\n public stream(contentType = \"text/plain\"): ResponseStreamController {\r\n // Set headers using the response API\r\n this.setContentType(contentType);\r\n this.header(\"Transfer-Encoding\", \"chunked\");\r\n this.header(\"Cache-Control\", \"no-cache\");\r\n this.header(\"Connection\", \"keep-alive\");\r\n this.header(\"X-Content-Type-Options\", \"nosniff\");\r\n\r\n // Trigger sending events\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n this.log(\"Starting stream\");\r\n\r\n // Track stream state\r\n let isEnded = false;\r\n const chunks: any[] = [];\r\n\r\n // Write headers to start the stream\r\n // Note: We use raw here because we need chunked encoding control\r\n // This is the only valid use case for bypassing Fastify's abstraction\r\n this.baseResponse.raw.writeHead(this.statusCode, this.getHeaders() as any);\r\n\r\n return {\r\n /**\r\n * Send a chunk of data to the client\r\n * @param data - Data to send (string, Buffer, or any serializable data)\r\n */\r\n send: (data: any) => {\r\n if (isEnded) {\r\n throw new Error(\"Cannot send data: stream has already ended\");\r\n }\r\n\r\n this.baseResponse.raw.write(data);\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * Render a React component and send it as a chunk\r\n * @param element - React element or component to render\r\n */\r\n render: (element: ReactNode) => {\r\n if (isEnded) {\r\n throw new Error(\"Cannot render: stream has already ended\");\r\n }\r\n\r\n const html = renderReact(element);\r\n chunks.push(html);\r\n this.baseResponse.raw.write(html);\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * End the stream and trigger completion events\r\n */\r\n end: () => {\r\n if (isEnded) {\r\n return this;\r\n }\r\n\r\n isEnded = true;\r\n\r\n // Store the streamed content for logging/debugging\r\n this.currentBody = chunks;\r\n this.parsedBody = chunks;\r\n\r\n // End the response\r\n this.baseResponse.raw.end();\r\n\r\n this.log(\"Stream ended\");\r\n\r\n // Trigger sent events\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // Trigger success event if status is 2xx\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n // Trigger status-specific events\r\n if (this.currentStatusCode === 201) {\r\n Response.trigger(\"successCreate\", this);\r\n }\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * Check if the stream has ended\r\n */\r\n get ended() {\r\n return isEnded;\r\n },\r\n };\r\n }\r\n\r\n /**\r\n * Create a Server-Sent Events (SSE) stream\r\n *\r\n * SSE is a standard for pushing real-time updates from server to client.\r\n * Perfect for live notifications, progress updates, or real-time data feeds.\r\n *\r\n * @example\r\n * ```ts\r\n * const sse = response.sse();\r\n *\r\n * // Send events\r\n * sse.send(\"message\", { text: \"Hello!\" });\r\n * sse.send(\"notification\", { type: \"info\", message: \"Update available\" }, \"msg-123\");\r\n *\r\n * // Keep connection alive\r\n * const keepAlive = setInterval(() => sse.comment(\"ping\"), 30000);\r\n *\r\n * // Clean up when done\r\n * clearInterval(keepAlive);\r\n * sse.end();\r\n * ```\r\n *\r\n * @returns SSE controller with send(), comment(), and end() methods\r\n */\r\n public sse(): ResponseSSEController {\r\n // Set SSE-specific headers\r\n this.setContentType(\"text/event-stream\");\r\n this.header(\"Cache-Control\", \"no-cache, no-store, must-revalidate\");\r\n this.header(\"Connection\", \"keep-alive\");\r\n this.header(\"X-Accel-Buffering\", \"no\"); // Disable nginx buffering\r\n\r\n // Trigger sending events\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n this.log(\"Starting SSE stream\");\r\n\r\n // Track stream state\r\n let isEnded = false;\r\n const events: any[] = [];\r\n const disconnectHandlers: Array<() => void> = [];\r\n\r\n // Write headers to start the stream\r\n this.baseResponse.raw.writeHead(this.statusCode, this.getHeaders() as any);\r\n\r\n // Detect client disconnect — set isEnded silently and invoke cleanup handlers.\r\n // Without this, background jobs keep writing to a dead socket after the client drops.\r\n this.baseResponse.raw.on(\"close\", () => {\r\n if (!isEnded) {\r\n isEnded = true;\r\n this.log(\"SSE client disconnected\");\r\n for (const handler of disconnectHandlers) {\r\n handler();\r\n }\r\n }\r\n });\r\n\r\n const controller: ResponseSSEController = {\r\n /**\r\n * Send an SSE event\r\n * @param event - Event name (e.g., \"message\", \"chunk\", \"done\")\r\n * @param data - Event data (will be JSON stringified)\r\n * @param id - Optional event ID for client-side Last-Event-ID tracking (reconnect support)\r\n */\r\n send: (event: string, data: any, id?: string): ResponseSSEController => {\r\n // Silent no-op after disconnect — background jobs should not crash when\r\n // the client drops mid-stream. The onDisconnect handler handles cleanup.\r\n if (isEnded) return controller;\r\n\r\n let message = \"\";\r\n if (id) message += `id: ${id}\\n`;\r\n message += `event: ${event}\\n`;\r\n message += `data: ${JSON.stringify(data)}\\n\\n`;\r\n\r\n events.push({ event, data, id });\r\n this.baseResponse.raw.write(message);\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * Send a comment (keeps connection alive, invisible to client)\r\n * Useful for preventing timeout on long-lived connections\r\n * @param text - Comment text\r\n */\r\n comment: (text: string): ResponseSSEController => {\r\n // Silent no-op after disconnect\r\n if (isEnded) return controller;\r\n\r\n this.baseResponse.raw.write(`: ${text}\\n\\n`);\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * End the SSE stream and trigger completion events\r\n */\r\n end: (): ResponseSSEController => {\r\n if (isEnded) return controller;\r\n\r\n isEnded = true;\r\n\r\n // Store the events for logging/debugging\r\n this.currentBody = events;\r\n this.parsedBody = events;\r\n\r\n // End the response\r\n this.baseResponse.raw.end();\r\n\r\n this.log(\"SSE stream ended\");\r\n\r\n // Trigger sent events\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // Trigger success event if status is 2xx\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * Register a handler to be called when the client disconnects.\r\n * Use this to clean up EventEmitter listeners, cancel background jobs, etc.\r\n *\r\n * @example\r\n * ```ts\r\n * const sse = response.sse();\r\n * const listener = (chunk) => sse.send(\"chunk\", { chunk });\r\n * eventBus.on(aiMessageId, listener);\r\n * sse.onDisconnect(() => eventBus.off(aiMessageId, listener));\r\n * ```\r\n */\r\n onDisconnect: (handler: () => void): ResponseSSEController => {\r\n disconnectHandlers.push(handler);\r\n return controller;\r\n },\r\n\r\n /**\r\n * Check if the stream has ended (either via end() or client disconnect)\r\n */\r\n get ended() {\r\n return isEnded;\r\n },\r\n };\r\n\r\n return controller;\r\n }\r\n\r\n /**\r\n * Set the status code\r\n */\r\n public setStatusCode(statusCode: number) {\r\n this.currentStatusCode = statusCode;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Redirect the user to another route\r\n */\r\n public redirect(url: string, statusCode = 302) {\r\n this.baseResponse.redirect(url, statusCode);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Permanent redirect\r\n */\r\n public permanentRedirect(url: string) {\r\n this.baseResponse.redirect(url, 301);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the response time\r\n */\r\n public getResponseTime() {\r\n return this.baseResponse.elapsedTime;\r\n }\r\n\r\n /**\r\n * Remove a specific header\r\n */\r\n public removeHeader(key: string) {\r\n this.baseResponse.removeHeader(key);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get a specific header\r\n */\r\n public getHeader(key: string) {\r\n return this.baseResponse.getHeader(key);\r\n }\r\n\r\n /**\r\n * Get the response headers\r\n */\r\n public getHeaders() {\r\n return this.baseResponse.getHeaders();\r\n }\r\n\r\n /**\r\n * Set multiple headers\r\n */\r\n public headers(headers: Record<string, string>) {\r\n this.baseResponse.headers(headers);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the response header\r\n */\r\n public header(key: string, value: any) {\r\n this.baseResponse.header(key, value);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set a cookie on the response.\r\n *\r\n * Values are JSON-stringified by default so structured cookies round-trip\r\n * cleanly with `request.cookie(name)`. Pass `{ raw: true }` to skip the\r\n * JSON wrapping for plain-string cookies (session tokens, opaque IDs).\r\n *\r\n * **Secure by default.** `httpOnly: true`, `sameSite: \"lax\"`, and — outside\r\n * development — `secure: true` are applied unless you override them. These\r\n * are the flags whose absence never fails a test and is fatal in production:\r\n * without `httpOnly` any injected script can read the cookie, without\r\n * `secure` it travels in cleartext, without `sameSite` it rides along on\r\n * cross-site requests. Opting out is explicit, per call or via\r\n * `http.cookies.options`.\r\n *\r\n * Precedence, lowest to highest: framework defaults → `http.cookies.options`\r\n * → the per-call `options` argument.\r\n *\r\n * @example\r\n * // JSON-wrapped (default) — round-trips with request.cookie()\r\n * response.cookie(\"prefs\", { theme: \"dark\" }, { maxAge: 3600 });\r\n *\r\n * @example\r\n * // Raw string — no JSON quoting; useful for tokens / opaque IDs\r\n * response.cookie(\"session\", \"abc.def.ghi\", { raw: true });\r\n *\r\n * @example\r\n * // Deliberately readable by client-side JS\r\n * response.cookie(\"theme\", \"dark\", { httpOnly: false });\r\n */\r\n public cookie(name: string, value: CookieValue, options: CookieOptions = {}) {\r\n const { raw, ...cookieOptions } = options;\r\n const defaultOptions = config.get(\"http.cookies.options\", {});\r\n const serializedValue = raw ? String(value) : JSON.stringify(value);\r\n\r\n this.baseResponse.setCookie(name, serializedValue, {\r\n ...secureCookieDefaults(),\r\n ...defaultOptions,\r\n ...cookieOptions,\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Clear a cookie from the response\r\n *\r\n * @example\r\n * response.clearCookie('token', { path: '/' });\r\n */\r\n public clearCookie(name: string, options?: CookieSerializeOptions) {\r\n const defaultOptions = config.get(\"http.cookies.options\", {});\r\n this.baseResponse.clearCookie(name, { ...defaultOptions, ...options });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Alias to header method\r\n */\r\n public setHeader(key: string, value: any) {\r\n return this.header(key, value);\r\n }\r\n\r\n /**\r\n * Send an error response with status code 500\r\n */\r\n public serverError(data: any) {\r\n return this.send(data, 500);\r\n }\r\n\r\n /**\r\n * Send a forbidden response with status code 403\r\n */\r\n public forbidden(\r\n data: any = {\r\n error: \"You are not allowed to access this resource, FORBIDDEN\",\r\n },\r\n ) {\r\n return this.send(data, 403);\r\n }\r\n\r\n /**\r\n * Send a service unavailable response with status code 503\r\n */\r\n public serviceUnavailable(data: any) {\r\n return this.send(data, 503);\r\n }\r\n\r\n /**\r\n * Send an unauthorized response with status code 401\r\n */\r\n public unauthorized(\r\n data: any = {\r\n error: \"unauthorized\",\r\n },\r\n ) {\r\n return this.send(data, 401);\r\n }\r\n\r\n /**\r\n * Send a not found response with status code 404\r\n */\r\n public notFound(\r\n data: any = {\r\n error: \"notFound\",\r\n },\r\n ) {\r\n return this.send(data, 404);\r\n }\r\n\r\n /**\r\n * Send a bad request response with status code 400\r\n */\r\n public badRequest(data: any) {\r\n return this.send(data, 400);\r\n }\r\n\r\n /**\r\n * Send a content too large response with status code 413\r\n */\r\n public contentTooLarge(data: any) {\r\n return this.send(data, 413);\r\n }\r\n\r\n /**\r\n * Send a success response with status code 201\r\n */\r\n public successCreate(data: any) {\r\n return this.send(data, 201);\r\n }\r\n\r\n /**\r\n * Send a success response\r\n */\r\n public success(data: any = { success: true }) {\r\n return this.send(data);\r\n }\r\n\r\n /**\r\n * Send a no content response with status code 204\r\n */\r\n public noContent() {\r\n return this.baseResponse.status(204).send();\r\n }\r\n\r\n /**\r\n * Send an accepted response with status code 202\r\n * Used for async operations that have been accepted but not yet processed\r\n */\r\n public accepted(data: any = { message: \"Request accepted for processing\" }) {\r\n return this.send(data, 202);\r\n }\r\n\r\n /**\r\n * Send a conflict response with status code 409\r\n */\r\n public conflict(data: any = { error: \"Resource conflict\" }) {\r\n return this.send(data, 409);\r\n }\r\n\r\n /**\r\n * Send a too many requests response with status code 429\r\n */\r\n public tooManyRequests(data: any) {\r\n return this.send(data, 429);\r\n }\r\n\r\n /**\r\n * Send an unprocessable entity response with status code 422\r\n * Used for semantic validation errors\r\n */\r\n public unprocessableEntity(data: any) {\r\n return this.send(data, 422);\r\n }\r\n\r\n /**\r\n * Apply response options (cache, disposition, etag)\r\n * Shared helper for sendFile and sendBuffer\r\n */\r\n /**\r\n * Build an RFC 6266–safe `Content-Disposition` value for a file name.\r\n *\r\n * HTTP header values must be ISO-8859-1; Node's `setHeader` throws\r\n * `ERR_INVALID_CHAR` on any other byte, so a raw `filename=\"تقرير.pdf\"` would\r\n * crash the response (a 500 the moment the name is non-ASCII). RFC 6266 solves\r\n * this with two parameters emitted together:\r\n * - `filename=\"<ascii>\"` — sanitised ASCII fallback for legacy clients\r\n * - `filename*=UTF-8''<pct>` — RFC 5987 ext-value; modern browsers restore\r\n * the real (e.g. Arabic) name.\r\n */\r\n private contentDisposition(type: \"inline\" | \"attachment\", rawName: string): string {\r\n // Strip CR/LF first so a crafted file name can never inject extra headers.\r\n const name = (rawName || \"file\").replace(/[\\r\\n]/g, \"\");\r\n\r\n // ASCII fallback: replace quote/backslash + every non-printable-ASCII byte\r\n // (covers all multibyte characters) so the quoted-string is always legal.\r\n const ascii =\r\n name\r\n .replace(/[\"\\\\]/g, \"_\")\r\n .replace(/[^\\x20-\\x7E]/g, \"_\")\r\n .trim() || \"file\";\r\n\r\n // Pure-ASCII name → the quoted form is enough; no ext-value needed.\r\n if (!/[^\\x20-\\x7E]/.test(name)) {\r\n return `${type}; filename=\"${ascii}\"`;\r\n }\r\n\r\n // RFC 5987 ext-value. `encodeURIComponent` leaves ' ( ) * unescaped, but they\r\n // are not valid `attr-char`, so percent-encode those too.\r\n const encoded = encodeURIComponent(name).replace(\r\n /['()*]/g,\r\n (char) => \"%\" + char.charCodeAt(0).toString(16).toUpperCase(),\r\n );\r\n\r\n return `${type}; filename=\"${ascii}\"; filename*=UTF-8''${encoded}`;\r\n }\r\n\r\n private applyResponseOptions(options: SendBufferOptions, defaultFilename?: string): boolean {\r\n // Set content type if provided\r\n if (options.contentType) {\r\n this.baseResponse.type(options.contentType);\r\n }\r\n\r\n // Set cache headers if specified\r\n if (options.cacheTime) {\r\n const cacheControl = options.immutable\r\n ? `public, max-age=${options.cacheTime}, immutable`\r\n : `public, max-age=${options.cacheTime}`;\r\n this.header(\"Cache-Control\", cacheControl);\r\n this.header(\"Expires\", new Date(Date.now() + options.cacheTime * 1000).toUTCString());\r\n }\r\n\r\n // Set ETag if provided (for conditional requests)\r\n if (options.etag) {\r\n this.header(\"ETag\", options.etag);\r\n\r\n // Check If-None-Match for conditional request\r\n const ifNoneMatch = this.request.header(\"if-none-match\");\r\n if (ifNoneMatch && ifNoneMatch === options.etag) {\r\n this.log(\"Content not modified (ETag match), sending 304\");\r\n this.baseResponse.status(304).send();\r\n return true; // Indicates 304 was sent\r\n }\r\n }\r\n\r\n // Set Content-Disposition if inline or filename is specified\r\n if (options.inline !== undefined || options.filename) {\r\n const disposition = options.inline ? \"inline\" : \"attachment\";\r\n const filename = options.filename || defaultFilename || \"file\";\r\n this.header(\"Content-Disposition\", this.contentDisposition(disposition, filename));\r\n }\r\n\r\n return false; // No 304 sent\r\n }\r\n\r\n /**\r\n * Send a file as a response\r\n */\r\n public async sendFile(filePath: string | StorageFile, options?: number | SendFileOptions) {\r\n if (filePath instanceof StorageFile) {\r\n filePath = filePath.absolutePath!;\r\n }\r\n\r\n this.log(`Sending file: ${filePath}`);\r\n\r\n // Check if file exists first\r\n if (!(await fileExistsAsync(filePath))) {\r\n return this.notFound({\r\n error: \"File Not Found\",\r\n });\r\n }\r\n\r\n try {\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Get file stats for ETag and Last-Modified\r\n const stats = await fs.promises.stat(filePath);\r\n const lastModified = stats.mtime;\r\n\r\n // Generate ETag based on file size and modification time\r\n const etag = `\"${stats.size}-${stats.mtime.getTime()}\"`;\r\n\r\n // Set Last-Modified header\r\n this.header(\"Last-Modified\", lastModified.toUTCString());\r\n this.header(\"ETag\", etag);\r\n\r\n // Set content type\r\n const contentType = this.getFileContentType(filePath);\r\n this.baseResponse.type(contentType);\r\n\r\n // Apply common response options (cache, disposition)\r\n const defaultFilename = path.basename(filePath);\r\n const sent304 = this.applyResponseOptions({ ...opts, etag, contentType }, defaultFilename);\r\n if (sent304) return this.baseResponse;\r\n\r\n // Check conditional request headers\r\n const ifNoneMatch = this.request.header(\"if-none-match\");\r\n const ifModifiedSince = this.request.header(\"if-modified-since\");\r\n\r\n // Handle If-None-Match (ETag validation)\r\n if (ifNoneMatch && ifNoneMatch === etag) {\r\n this.log(\"File not modified (ETag match), sending 304\");\r\n return this.baseResponse.status(304).send();\r\n }\r\n\r\n // Handle If-Modified-Since (Last-Modified validation)\r\n if (ifModifiedSince) {\r\n const modifiedSinceDate = new Date(ifModifiedSince);\r\n if (lastModified.getTime() <= modifiedSinceDate.getTime()) {\r\n this.log(\"File not modified (Last-Modified check), sending 304\");\r\n return this.baseResponse.status(304).send();\r\n }\r\n }\r\n\r\n // Use streaming for efficient file sending\r\n const stream = fs.createReadStream(filePath);\r\n\r\n // Handle stream errors\r\n stream.on(\"error\", (error) => {\r\n this.log(`Error reading file: ${error.message}`, \"error\");\r\n if (!this.baseResponse.sent) {\r\n this.serverError({\r\n error: \"Error reading file\",\r\n message: error.message,\r\n });\r\n }\r\n });\r\n\r\n // Send the stream (endTime will be set by finish event listener)\r\n return this.baseResponse.send(stream);\r\n } catch (error: any) {\r\n this.log(`Error sending file: ${error.message}`, \"error\");\r\n return this.serverError({\r\n error: \"Error sending file\",\r\n message: error.message,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Send buffer as a response\r\n * Useful for dynamically generated content (e.g., resized images, generated PDFs)\r\n */\r\n public sendBuffer(buffer: Buffer, options?: number | SendBufferOptions) {\r\n this.log(\"Sending buffer\");\r\n\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Apply common response options (cache, disposition, etag)\r\n const sent304 = this.applyResponseOptions(opts);\r\n if (sent304) return this.baseResponse;\r\n\r\n // Note: endTime is set in the main send() method for non-streaming responses\r\n return this.baseResponse.send(buffer);\r\n }\r\n\r\n /**\r\n * Send an Image instance as a response\r\n * Automatically detects image format and sets content type\r\n */\r\n public async sendImage(\r\n image: any, // Type as 'any' to avoid circular dependency with Image class\r\n options?: number | (Omit<SendBufferOptions, \"contentType\"> & { contentType?: string }),\r\n ) {\r\n this.log(\"Sending image\");\r\n\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Get image metadata to determine format\r\n const metadata = await image.metadata();\r\n const format = metadata.format || \"jpeg\";\r\n\r\n // Convert image to buffer\r\n const buffer = await image.toBuffer();\r\n\r\n // Auto-set content type if not provided\r\n const contentType = opts.contentType || `image/${format}`;\r\n\r\n // Auto-generate ETag if not provided\r\n // Format: \"format-widthxheight-size\" (e.g., \"jpeg-800x600-45231\")\r\n // This catches changes in dimensions, quality, filters, and format\r\n if (!opts.etag) {\r\n const width = metadata.width || 0;\r\n const height = metadata.height || 0;\r\n opts.etag = `\"${format}-${width}x${height}-${buffer.length}\"`;\r\n }\r\n\r\n // Apply common response options with auto-detected content type\r\n const sent304 = this.applyResponseOptions({ ...opts, contentType });\r\n if (sent304) return this.baseResponse;\r\n\r\n // Note: endTime is set in the main send() method for non-streaming responses\r\n return this.baseResponse.send(buffer);\r\n }\r\n\r\n /**\r\n * Send file and cache it\r\n * Cache time in seconds\r\n * Cache time will be one year\r\n */\r\n public sendCachedFile(path: string | StorageFile, cacheTime = 31536000) {\r\n return this.sendFile(path, cacheTime);\r\n }\r\n\r\n /**\r\n * Download the given file path\r\n */\r\n public download(path: string, filename?: string) {\r\n return this.downloadFile(path, filename);\r\n }\r\n\r\n /**\r\n * Download the given file path\r\n */\r\n public async downloadFile(filePath: string, filename?: string) {\r\n // Check if file exists first\r\n if (!(await fileExistsAsync(filePath))) {\r\n return this.notFound({\r\n error: \"File Not Found\",\r\n });\r\n }\r\n\r\n try {\r\n if (!filename) {\r\n filename = path.basename(filePath);\r\n }\r\n\r\n this.baseResponse.header(\r\n \"Content-Disposition\",\r\n this.contentDisposition(\"attachment\", filename),\r\n );\r\n\r\n // this.baseResponse.header(\"Content-Type\", this.getFileContentType(filePath));\r\n this.baseResponse.header(\"Content-Type\", \"application/octet-stream\");\r\n\r\n const stream = fs.createReadStream(filePath);\r\n\r\n // Handle stream errors\r\n stream.on(\"error\", (error) => {\r\n this.log(`Error reading file for download: ${error.message}`, \"error\");\r\n if (!this.baseResponse.sent) {\r\n this.serverError({\r\n error: \"Error reading file\",\r\n message: error.message,\r\n });\r\n }\r\n });\r\n\r\n // Send the stream (endTime will be set by finish event listener)\r\n return this.baseResponse.send(stream);\r\n } catch (error: any) {\r\n this.log(`Error downloading file: ${error.message}`, \"error\");\r\n return this.serverError({\r\n error: \"Error downloading file\",\r\n message: error.message,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Get content type of the given path\r\n */\r\n public getFileContentType(filePath: string) {\r\n const type = mime.getType(filePath) || \"application/octet-stream\";\r\n return type;\r\n }\r\n\r\n /**\r\n * Mark the response as failed\r\n */\r\n public failedSchema(result: ValidationResult) {\r\n const { errors, inputKey, inputError, status } = config.get(\"validation.response\", {\r\n errors: \"errors\",\r\n inputKey: \"input\",\r\n inputError: \"error\",\r\n status: 422,\r\n });\r\n\r\n log.error(\"request\", \"validation\", `${this.request.id} - Validation failed`);\r\n\r\n return this.send(\r\n {\r\n [errors]: result.errors.map((error) => ({\r\n [inputKey]: error.input,\r\n [inputError]: error.error,\r\n })),\r\n },\r\n status,\r\n );\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"response.mjs","names":[],"sources":["../../../../../../../core/src/http/response.ts"],"sourcesContent":["import type { CookieSerializeOptions } from \"@fastify/cookie\";\r\nimport config from \"@mongez/config\";\r\nimport type { EventSubscription } from \"@mongez/events\";\r\nimport events from \"@mongez/events\";\r\nimport { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { isIterable, isPlainObject, isScalar } from \"@mongez/supportive-is\";\r\nimport type { LogLevel } from \"@warlock.js/logger\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { ValidationResult } from \"@warlock.js/seal\";\r\nimport type { FastifyReply } from \"fastify\";\r\nimport fs from \"fs\";\r\nimport mime from \"mime\";\r\nimport path from \"path\";\r\nimport type React from \"react\";\r\nimport { type ReactNode } from \"react\";\r\nimport { Application } from \"../application/application\";\r\nimport { LOCALE_COOKIE_NAME, resolveLocaleConfiguration } from \"../config/locale-configuration\";\r\nimport { UnknownLocaleError } from \"../errors/unknown-locale-error\";\r\nimport type { Route } from \"../router\";\r\nimport { StorageFile } from \"../storage\";\r\nimport { renderReact } from \"./../react\";\r\nimport type { Request } from \"./request\";\r\nimport type { ResponseEvent, ResponseSSEController, ResponseStreamController } from \"./types\";\r\n\r\ntype CookieValue = string | number | boolean | Record<string, any> | Array<any>;\r\n\r\n/**\r\n * Cookie options accepted by `response.cookie()`.\r\n *\r\n * Extends Fastify's `CookieSerializeOptions` with `raw` — set to `true` to\r\n * skip the default `JSON.stringify` of the value and write it as-is. Use for\r\n * plain-string cookies (session tokens, opaque IDs) that shouldn't be JSON-quoted.\r\n *\r\n * When `raw: true`, non-string values are coerced via `String(value)`. The\r\n * read side (`request.cookie(name)`) tries `JSON.parse` first and falls back\r\n * to the raw string on parse failure, so round-tripping a raw string cookie\r\n * Just Works.\r\n */\r\nexport type CookieOptions = CookieSerializeOptions & {\r\n /**\r\n * Skip JSON.stringify and write the value as-is.\r\n *\r\n * @default false\r\n */\r\n raw?: boolean;\r\n};\r\n\r\nexport enum ResponseStatus {\r\n OK = 200,\r\n CREATED = 201,\r\n ACCEPTED = 202,\r\n MOVED_PERMANENTLY = 301,\r\n FOUND = 302,\r\n SEE_OTHER = 303,\r\n NOT_MODIFIED = 304,\r\n TEMPORARY_REDIRECT = 307,\r\n PERMANENT_REDIRECT = 308,\r\n NO_CONTENT = 204,\r\n BAD_REQUEST = 400,\r\n UNAUTHORIZED = 401,\r\n FORBIDDEN = 403,\r\n NOT_FOUND = 404,\r\n METHOD_NOT_ALLOWED = 405,\r\n CONFLICT = 409,\r\n TOO_MANY_REQUESTS = 429,\r\n INTERNAL_SERVER_ERROR = 500,\r\n SERVICE_UNAVAILABLE = 503,\r\n}\r\n\r\n/**\r\n * Options for sending files\r\n */\r\nexport type SendFileOptions = {\r\n cacheTime?: number;\r\n immutable?: boolean;\r\n inline?: boolean;\r\n filename?: string;\r\n};\r\n\r\n/**\r\n * Options for sending buffers\r\n */\r\nexport type SendBufferOptions = SendFileOptions & {\r\n contentType?: string;\r\n etag?: string;\r\n};\r\n\r\n/**\r\n * The cookie flags every response cookie gets unless something overrides them.\r\n *\r\n * Computed per call rather than hoisted to a constant because `secure` depends\r\n * on the environment, which is not known at module-evaluation time.\r\n *\r\n * `secure` is relaxed in development only: a `Secure` cookie is dropped by the\r\n * browser over plain http, which would silently break every local login. It\r\n * stays on everywhere else, including test and staging.\r\n */\r\nfunction secureCookieDefaults(): CookieSerializeOptions {\r\n return {\r\n httpOnly: true,\r\n sameSite: \"lax\",\r\n secure: !Application.isDevelopment,\r\n };\r\n}\r\n\r\nexport class Response {\r\n /**\r\n * Current route\r\n */\r\n protected route!: Route;\r\n\r\n /**\r\n * Underlying Fastify reply — a public escape hatch to capabilities the\r\n * framework's high-level helpers don't yet cover.\r\n *\r\n * **Prefer framework methods first**: `response.send()`, `response.header()`,\r\n * `response.cookie()`, `response.sendFile()`, `response.stream()`, etc.\r\n * They wire status codes, content-type detection, the event lifecycle, and\r\n * the cache-pattern replay path correctly.\r\n *\r\n * **Reach for `baseResponse` only** when the framework genuinely lacks a\r\n * helper for what you need — and when you do, file an issue so we can add\r\n * it. Streaming and SSE are the precedent here: they bypass `send()`\r\n * deliberately because the framework didn't ship chunked-write support\r\n * natively at the time. Reaching here for non-streaming work means a\r\n * missing helper, not an answer.\r\n */\r\n public baseResponse!: FastifyReply;\r\n\r\n /**\r\n * Current status code\r\n */\r\n protected currentStatusCode = 200;\r\n\r\n /**\r\n * Current response body\r\n */\r\n protected currentBody: any;\r\n\r\n /**\r\n * Request object\r\n */\r\n public request!: Request;\r\n\r\n /**\r\n * Internal events related to this particular response object\r\n */\r\n protected events = new Map<string, any[]>();\r\n\r\n /**\r\n * Parsed body\r\n * This will return the parsed body of the response\r\n * Please note that if this property is called before the response is sent, it will return undefined\r\n */\r\n public parsedBody: any;\r\n\r\n /**\r\n * Get raw response\r\n */\r\n public get raw() {\r\n return this.baseResponse.raw;\r\n }\r\n\r\n /**\r\n * Get Current response body\r\n */\r\n public get body() {\r\n return this.currentBody;\r\n }\r\n\r\n /**\r\n * Set response body\r\n */\r\n public set body(body: any) {\r\n this.currentBody = body;\r\n }\r\n\r\n /**\r\n * Add event on sending response\r\n */\r\n public onSending(callback: any) {\r\n this.events.set(\"sending\", [...(this.events.get(\"sending\") || []), callback]);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Add event on sent response\r\n */\r\n public onSent(callback: any) {\r\n this.events.set(\"sent\", [...(this.events.get(\"sent\") || []), callback]);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the Fastify response object\r\n */\r\n public setResponse(response: FastifyReply) {\r\n this.baseResponse = response;\r\n\r\n // Listen to the 'finish' event to track when response is fully sent\r\n // This works for all response types: JSON, streams, buffers, files, etc.\r\n this.baseResponse.raw.once(\"finish\", () => {\r\n this.request.endTime = Date.now();\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Reset the response state\r\n */\r\n public reset() {\r\n this.route = {} as Route;\r\n this.currentBody = null;\r\n this.currentStatusCode = 200;\r\n }\r\n\r\n /**\r\n * Set current route\r\n */\r\n public setRoute(route: Route) {\r\n this.route = route;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the content type\r\n */\r\n public get contentType() {\r\n return this.baseResponse.getHeader(\"Content-Type\");\r\n }\r\n\r\n /**\r\n * Set the content type\r\n */\r\n public setContentType(contentType: string) {\r\n this.baseResponse.header(\"Content-Type\", contentType);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the status code\r\n */\r\n public get statusCode(): number {\r\n return this.currentStatusCode ?? this.baseResponse.statusCode;\r\n }\r\n\r\n /**\r\n * Check if response status is ok\r\n */\r\n public get isOk() {\r\n return this.currentStatusCode >= 200 && this.currentStatusCode < 300;\r\n }\r\n\r\n /**\r\n * Check if the response has been sent\r\n */\r\n public get sent() {\r\n return this.baseResponse.sent;\r\n }\r\n\r\n /**\r\n * Add a listener to the response event\r\n */\r\n public static on(\r\n event: ResponseEvent,\r\n listener: (response: Response) => void,\r\n ): EventSubscription {\r\n return events.subscribe(`response.${event}`, listener);\r\n }\r\n\r\n /**\r\n * Trigger the response event\r\n */\r\n protected static async trigger(event: ResponseEvent, ...args: any[]) {\r\n // make a timeout to make sure the request events is executed first\r\n return new Promise((resolve) => {\r\n setTimeout(async () => {\r\n await events.triggerAllAsync(`response.${event}`, ...args);\r\n resolve(true);\r\n }, 0);\r\n });\r\n }\r\n\r\n /**\r\n * Parse body\r\n */\r\n protected async parseBody() {\r\n return await this.parse(this.currentBody);\r\n }\r\n\r\n /**\r\n * Parse the given value\r\n */\r\n public async parse(value: any): Promise<any> {\r\n // if it is a falsy value, return it\r\n if (!value || isScalar(value)) return value;\r\n\r\n // if it has a `toJSON` method, call it and await the result then return it\r\n if (value.toJSON) {\r\n value.request = this.request;\r\n return await value.toJSON();\r\n }\r\n\r\n // if it is iterable, an array or array-like object then parse each item\r\n if (isIterable(value)) {\r\n const values = Array.from(value);\r\n\r\n return Promise.all(\r\n values.map(async (item: any) => {\r\n return await this.parse(item);\r\n }),\r\n );\r\n }\r\n\r\n // if not plain object, then return it\r\n if (!isPlainObject(value)) {\r\n return value;\r\n }\r\n\r\n // loop over the object and check if the value and call `parse` on it\r\n for (const key in value) {\r\n const subValue = value[key];\r\n\r\n value[key] = await this.parse(subValue);\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * Make a log message\r\n */\r\n public log(message: string, level: LogLevel = \"info\") {\r\n if (!config.get(\"http.log\")) return;\r\n\r\n log.log({\r\n module: \"response\",\r\n action: this.route.method + \" \" + this.route.path.replace(\"/*\", \"\") + `:${this.request.id}`,\r\n message,\r\n type: level,\r\n context: {\r\n request: this.request,\r\n response: this,\r\n },\r\n });\r\n }\r\n\r\n /**\r\n * Check if returning response is json\r\n */\r\n public get isJson() {\r\n return this.getHeader(\"Content-Type\") === \"application/json\";\r\n }\r\n\r\n /**\r\n * Send the response\r\n * @param data - Response data\r\n * @param statusCode - HTTP status code\r\n * @param triggerEvents - Whether to trigger response events (default: true)\r\n */\r\n public async send(data?: any, statusCode?: number, triggerEvents = true): Promise<Response> {\r\n // Defensive guard against double-send. The underlying Fastify reply silently\r\n // ignores subsequent sends once `sent === true`, which has historically hidden\r\n // middleware bugs (cache-pattern replay paths that returned `baseResponse.send`\r\n // ended up re-entering `Response.send` with the FastifyReply as the body).\r\n // Surfacing the misuse via `error`-level log makes the bug loud without\r\n // crashing production traffic.\r\n if (this.baseResponse.sent) {\r\n log.error(\r\n \"response\",\r\n \"send\",\r\n `send() called on already-sent response (request:${this.request?.id ?? \"unknown\"}) — likely a middleware bug`,\r\n );\r\n\r\n return this;\r\n }\r\n\r\n if (statusCode) {\r\n this.currentStatusCode = statusCode;\r\n }\r\n\r\n if (data === this) return this;\r\n\r\n if (data) {\r\n this.currentBody = data;\r\n }\r\n\r\n if (!this.currentStatusCode) {\r\n this.currentStatusCode = 200;\r\n }\r\n\r\n this.log(\"Sending response\");\r\n // Auto-pick `application/json` only when no content-type was set by the caller.\r\n // This preserves explicit overrides (e.g. `application/vnd.api+json` from a\r\n // cache replay, `application/problem+json` from an RFC 7807 error response)\r\n // while keeping the convenience default for the common object-body path.\r\n if (Array.isArray(this.currentBody) || isPlainObject(this.currentBody)) {\r\n if (!this.baseResponse.getHeader(\"Content-Type\")) {\r\n this.setContentType(\"application/json\");\r\n }\r\n }\r\n\r\n if (triggerEvents) {\r\n await Response.trigger(\"sending\", this);\r\n\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n await callback(this);\r\n }\r\n\r\n if (this.isJson) {\r\n await Response.trigger(\"sendingJson\", this);\r\n for (const callback of this.events.get(\"sendingJson\") || []) {\r\n await callback(this);\r\n }\r\n\r\n if (this.isOk) {\r\n await Response.trigger(\"sendingSuccessJson\", this);\r\n for (const callback of this.events.get(\"sendingSuccessJson\") || []) {\r\n await callback(this);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // parse the body and make sure it is transformed to sync data instead of async data\r\n if (typeof this.currentBody !== \"string\") {\r\n this.parsedBody = await this.parseBody();\r\n } else {\r\n this.parsedBody = data;\r\n }\r\n\r\n // Set the status first\r\n this.baseResponse.status(this.currentStatusCode);\r\n\r\n // Then send the response with the parsed body\r\n await this.baseResponse.send(this.parsedBody);\r\n\r\n this.log(\"Response sent\");\r\n\r\n if (triggerEvents) {\r\n // trigger the sent event\r\n Response.trigger(\"sent\", this);\r\n\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // trigger the success event if the status code is 2xx\r\n if (this.currentStatusCode >= 200 && this.currentStatusCode < 300) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n // trigger the successCreate event if the status code is 201\r\n if (this.currentStatusCode === 201) {\r\n Response.trigger(\"successCreate\", this);\r\n }\r\n\r\n // trigger the badRequest event if the status code is 400\r\n if (this.currentStatusCode === 400) {\r\n Response.trigger(\"badRequest\", this);\r\n }\r\n\r\n // trigger the unauthorized event if the status code is 401\r\n if (this.currentStatusCode === 401) {\r\n Response.trigger(\"unauthorized\", this);\r\n }\r\n\r\n // trigger the forbidden event if the status code is 403\r\n if (this.currentStatusCode === 403) {\r\n Response.trigger(\"forbidden\", this);\r\n }\r\n\r\n // trigger the notFound event if the status code is 404\r\n if (this.currentStatusCode === 404) {\r\n Response.trigger(\"notFound\", this);\r\n }\r\n\r\n // trigger the content too large event if the status code is 413\r\n if (this.currentStatusCode === 413) {\r\n Response.trigger(\"contentTooLarge\", this);\r\n }\r\n\r\n // trigger the throttled event if the status code is 429\r\n if (this.currentStatusCode === 429) {\r\n Response.trigger(\"throttled\", this);\r\n }\r\n\r\n // trigger the serverError event if the status code is 500\r\n if (this.currentStatusCode === 500) {\r\n Response.trigger(\"serverError\", this);\r\n }\r\n\r\n // trigger the error event if the status code is 4xx or 5xx\r\n if (this.currentStatusCode >= 400) {\r\n Response.trigger(\"error\", this);\r\n }\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Replay a previously-captured response shape — used by cache-pattern\r\n * middlewares (idempotency, response cache) to send a cached response\r\n * without re-running the controller.\r\n *\r\n * Preserves the cached status code, content-type, and any extra headers,\r\n * then sends the body through the standard `send()` pipeline so the full\r\n * event lifecycle still fires (`sent`, `success`, status-specific events).\r\n * That keeps cross-cutting observers (logger, metrics, audit) consistent\r\n * between fresh and replayed responses.\r\n *\r\n * @example\r\n * // Inside a cache-pattern middleware on HIT:\r\n * return response.header(\"X-Cache\", \"HIT\").replay({\r\n * status: cached.status,\r\n * body: cached.body,\r\n * contentType: cached.contentType,\r\n * });\r\n */\r\n public replay(cached: {\r\n status: number;\r\n body: unknown;\r\n contentType?: string;\r\n headers?: Record<string, string>;\r\n }): Promise<Response> {\r\n this.setStatusCode(cached.status);\r\n\r\n if (cached.contentType) {\r\n this.setContentType(cached.contentType);\r\n }\r\n\r\n if (cached.headers) {\r\n for (const [name, value] of Object.entries(cached.headers)) {\r\n this.header(name, value);\r\n }\r\n }\r\n\r\n return this.send(cached.body);\r\n }\r\n\r\n /**\r\n * Send html response\r\n */\r\n public html(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/html\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Render the given react component\r\n */\r\n public render(element: React.ReactElement | React.ComponentType, status = 200) {\r\n return this.setStatusCode(status).html(renderReact(element));\r\n }\r\n\r\n /**\r\n * Send xml response\r\n */\r\n public xml(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/xml\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Send plain text response\r\n */\r\n public text(data: string, statusCode?: number) {\r\n return this.setContentType(\"text/plain\").send(data, statusCode);\r\n }\r\n\r\n /**\r\n * Create a streaming response for progressive/chunked data sending\r\n *\r\n * This method allows you to send data in chunks and control when the response ends.\r\n * Perfect for Server-Sent Events (SSE), progressive rendering, or streaming large responses.\r\n *\r\n * @example\r\n * ```ts\r\n * const stream = response.stream(\"text/html\");\r\n * stream.send(\"<html><body>\");\r\n * stream.send(\"<h1>Hello</h1>\");\r\n * stream.render(<MyComponent />);\r\n * stream.send(\"</body></html>\");\r\n * stream.end();\r\n * ```\r\n *\r\n * @param contentType - The content type for the stream (default: \"text/plain\")\r\n * @returns Stream controller with send(), render(), and end() methods\r\n */\r\n public stream(contentType = \"text/plain\"): ResponseStreamController {\r\n // Set headers using the response API\r\n this.setContentType(contentType);\r\n this.header(\"Transfer-Encoding\", \"chunked\");\r\n this.header(\"Cache-Control\", \"no-cache\");\r\n this.header(\"Connection\", \"keep-alive\");\r\n this.header(\"X-Content-Type-Options\", \"nosniff\");\r\n\r\n // Trigger sending events\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n this.log(\"Starting stream\");\r\n\r\n // Track stream state\r\n let isEnded = false;\r\n const chunks: any[] = [];\r\n\r\n // Write headers to start the stream\r\n // Note: We use raw here because we need chunked encoding control\r\n // This is the only valid use case for bypassing Fastify's abstraction\r\n this.baseResponse.raw.writeHead(this.statusCode, this.getHeaders() as any);\r\n\r\n return {\r\n /**\r\n * Send a chunk of data to the client\r\n * @param data - Data to send (string, Buffer, or any serializable data)\r\n */\r\n send: (data: any) => {\r\n if (isEnded) {\r\n throw new Error(\"Cannot send data: stream has already ended\");\r\n }\r\n\r\n this.baseResponse.raw.write(data);\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * Render a React component and send it as a chunk\r\n * @param element - React element or component to render\r\n */\r\n render: (element: ReactNode) => {\r\n if (isEnded) {\r\n throw new Error(\"Cannot render: stream has already ended\");\r\n }\r\n\r\n const html = renderReact(element);\r\n chunks.push(html);\r\n this.baseResponse.raw.write(html);\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * End the stream and trigger completion events\r\n */\r\n end: () => {\r\n if (isEnded) {\r\n return this;\r\n }\r\n\r\n isEnded = true;\r\n\r\n // Store the streamed content for logging/debugging\r\n this.currentBody = chunks;\r\n this.parsedBody = chunks;\r\n\r\n // End the response\r\n this.baseResponse.raw.end();\r\n\r\n this.log(\"Stream ended\");\r\n\r\n // Trigger sent events\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // Trigger success event if status is 2xx\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n // Trigger status-specific events\r\n if (this.currentStatusCode === 201) {\r\n Response.trigger(\"successCreate\", this);\r\n }\r\n\r\n return this;\r\n },\r\n\r\n /**\r\n * Check if the stream has ended\r\n */\r\n get ended() {\r\n return isEnded;\r\n },\r\n };\r\n }\r\n\r\n /**\r\n * Create a Server-Sent Events (SSE) stream\r\n *\r\n * SSE is a standard for pushing real-time updates from server to client.\r\n * Perfect for live notifications, progress updates, or real-time data feeds.\r\n *\r\n * @example\r\n * ```ts\r\n * const sse = response.sse();\r\n *\r\n * // Send events\r\n * sse.send(\"message\", { text: \"Hello!\" });\r\n * sse.send(\"notification\", { type: \"info\", message: \"Update available\" }, \"msg-123\");\r\n *\r\n * // Keep connection alive\r\n * const keepAlive = setInterval(() => sse.comment(\"ping\"), 30000);\r\n *\r\n * // Clean up when done\r\n * clearInterval(keepAlive);\r\n * sse.end();\r\n * ```\r\n *\r\n * @returns SSE controller with send(), comment(), and end() methods\r\n */\r\n public sse(): ResponseSSEController {\r\n // Set SSE-specific headers\r\n this.setContentType(\"text/event-stream\");\r\n this.header(\"Cache-Control\", \"no-cache, no-store, must-revalidate\");\r\n this.header(\"Connection\", \"keep-alive\");\r\n this.header(\"X-Accel-Buffering\", \"no\"); // Disable nginx buffering\r\n\r\n // Trigger sending events\r\n Response.trigger(\"sending\", this);\r\n for (const callback of this.events.get(\"sending\") || []) {\r\n callback(this);\r\n }\r\n\r\n this.log(\"Starting SSE stream\");\r\n\r\n // Track stream state\r\n let isEnded = false;\r\n const events: any[] = [];\r\n const disconnectHandlers: Array<() => void> = [];\r\n\r\n // Write headers to start the stream\r\n this.baseResponse.raw.writeHead(this.statusCode, this.getHeaders() as any);\r\n\r\n // Detect client disconnect — set isEnded silently and invoke cleanup handlers.\r\n // Without this, background jobs keep writing to a dead socket after the client drops.\r\n this.baseResponse.raw.on(\"close\", () => {\r\n if (!isEnded) {\r\n isEnded = true;\r\n this.log(\"SSE client disconnected\");\r\n for (const handler of disconnectHandlers) {\r\n handler();\r\n }\r\n }\r\n });\r\n\r\n const controller: ResponseSSEController = {\r\n /**\r\n * Send an SSE event\r\n * @param event - Event name (e.g., \"message\", \"chunk\", \"done\")\r\n * @param data - Event data (will be JSON stringified)\r\n * @param id - Optional event ID for client-side Last-Event-ID tracking (reconnect support)\r\n */\r\n send: (event: string, data: any, id?: string): ResponseSSEController => {\r\n // Silent no-op after disconnect — background jobs should not crash when\r\n // the client drops mid-stream. The onDisconnect handler handles cleanup.\r\n if (isEnded) return controller;\r\n\r\n let message = \"\";\r\n if (id) message += `id: ${id}\\n`;\r\n message += `event: ${event}\\n`;\r\n message += `data: ${JSON.stringify(data)}\\n\\n`;\r\n\r\n events.push({ event, data, id });\r\n this.baseResponse.raw.write(message);\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * Send a comment (keeps connection alive, invisible to client)\r\n * Useful for preventing timeout on long-lived connections\r\n * @param text - Comment text\r\n */\r\n comment: (text: string): ResponseSSEController => {\r\n // Silent no-op after disconnect\r\n if (isEnded) return controller;\r\n\r\n this.baseResponse.raw.write(`: ${text}\\n\\n`);\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * End the SSE stream and trigger completion events\r\n */\r\n end: (): ResponseSSEController => {\r\n if (isEnded) return controller;\r\n\r\n isEnded = true;\r\n\r\n // Store the events for logging/debugging\r\n this.currentBody = events;\r\n this.parsedBody = events;\r\n\r\n // End the response\r\n this.baseResponse.raw.end();\r\n\r\n this.log(\"SSE stream ended\");\r\n\r\n // Trigger sent events\r\n Response.trigger(\"sent\", this);\r\n for (const callback of this.events.get(\"sent\") || []) {\r\n callback(this);\r\n }\r\n\r\n // Trigger success event if status is 2xx\r\n if (this.isOk) {\r\n Response.trigger(\"success\", this);\r\n }\r\n\r\n return controller;\r\n },\r\n\r\n /**\r\n * Register a handler to be called when the client disconnects.\r\n * Use this to clean up EventEmitter listeners, cancel background jobs, etc.\r\n *\r\n * @example\r\n * ```ts\r\n * const sse = response.sse();\r\n * const listener = (chunk) => sse.send(\"chunk\", { chunk });\r\n * eventBus.on(aiMessageId, listener);\r\n * sse.onDisconnect(() => eventBus.off(aiMessageId, listener));\r\n * ```\r\n */\r\n onDisconnect: (handler: () => void): ResponseSSEController => {\r\n disconnectHandlers.push(handler);\r\n return controller;\r\n },\r\n\r\n /**\r\n * Check if the stream has ended (either via end() or client disconnect)\r\n */\r\n get ended() {\r\n return isEnded;\r\n },\r\n };\r\n\r\n return controller;\r\n }\r\n\r\n /**\r\n * Set the status code\r\n */\r\n public setStatusCode(statusCode: number) {\r\n this.currentStatusCode = statusCode;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Redirect the user to another route\r\n */\r\n public redirect(url: string, statusCode = 302) {\r\n this.baseResponse.redirect(url, statusCode);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Permanent redirect\r\n */\r\n public permanentRedirect(url: string) {\r\n this.baseResponse.redirect(url, 301);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get the response time\r\n */\r\n public getResponseTime() {\r\n return this.baseResponse.elapsedTime;\r\n }\r\n\r\n /**\r\n * Remove a specific header\r\n */\r\n public removeHeader(key: string) {\r\n this.baseResponse.removeHeader(key);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get a specific header\r\n */\r\n public getHeader(key: string) {\r\n return this.baseResponse.getHeader(key);\r\n }\r\n\r\n /**\r\n * Get the response headers\r\n */\r\n public getHeaders() {\r\n return this.baseResponse.getHeaders();\r\n }\r\n\r\n /**\r\n * Set multiple headers\r\n */\r\n public headers(headers: Record<string, string>) {\r\n this.baseResponse.headers(headers);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the response header\r\n */\r\n public header(key: string, value: any) {\r\n this.baseResponse.header(key, value);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set a cookie on the response.\r\n *\r\n * Values are JSON-stringified by default so structured cookies round-trip\r\n * cleanly with `request.cookie(name)`. Pass `{ raw: true }` to skip the\r\n * JSON wrapping for plain-string cookies (session tokens, opaque IDs).\r\n *\r\n * **Secure by default.** `httpOnly: true`, `sameSite: \"lax\"`, and — outside\r\n * development — `secure: true` are applied unless you override them. These\r\n * are the flags whose absence never fails a test and is fatal in production:\r\n * without `httpOnly` any injected script can read the cookie, without\r\n * `secure` it travels in cleartext, without `sameSite` it rides along on\r\n * cross-site requests. Opting out is explicit, per call or via\r\n * `http.cookies.options`.\r\n *\r\n * Precedence, lowest to highest: framework defaults → `http.cookies.options`\r\n * → the per-call `options` argument.\r\n *\r\n * @example\r\n * // JSON-wrapped (default) — round-trips with request.cookie()\r\n * response.cookie(\"prefs\", { theme: \"dark\" }, { maxAge: 3600 });\r\n *\r\n * @example\r\n * // Raw string — no JSON quoting; useful for tokens / opaque IDs\r\n * response.cookie(\"session\", \"abc.def.ghi\", { raw: true });\r\n *\r\n * @example\r\n * // Deliberately readable by client-side JS\r\n * response.cookie(\"theme\", \"dark\", { httpOnly: false });\r\n */\r\n public cookie(name: string, value: CookieValue, options: CookieOptions = {}) {\r\n const { raw, ...cookieOptions } = options;\r\n const defaultOptions = config.get(\"http.cookies.options\", {});\r\n const serializedValue = raw ? String(value) : JSON.stringify(value);\r\n\r\n this.baseResponse.setCookie(name, serializedValue, {\r\n ...secureCookieDefaults(),\r\n ...defaultOptions,\r\n ...cookieOptions,\r\n });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set the active locale for subsequent requests.\r\n *\r\n * Writes the SAME cookie `request.locale` reads (`LOCALE_COOKIE_NAME`,\r\n * owned by the framework, never a string an app hardcodes) — the two sides\r\n * are read from one shared constant so they cannot name the cookie\r\n * differently. Written raw (no JSON quoting), matching how\r\n * `Request.resolveLocale()` reads it back.\r\n *\r\n * Any `Set-Cookie` this emits revokes public cacheability for the response,\r\n * including a page that opted into `public, max-age` — a per-visitor\r\n * locale cookie replayed from a shared cache would hand visitor A's locale\r\n * to visitor B.\r\n *\r\n * @throws {UnknownLocaleError} when `locale` is outside the app's\r\n * configured `app.localeCodes` allow-list. Silently accepting an\r\n * unconfigured locale would set a cookie the app can never actually serve.\r\n *\r\n * @example\r\n * response.setLocale(\"ar\");\r\n */\r\n public setLocale(locale: string) {\r\n const { localeCodes } = resolveLocaleConfiguration(\r\n config.get(\"app.localeCode\"),\r\n config.get(\"app.localeCodes\"),\r\n );\r\n\r\n if (localeCodes !== undefined && !localeCodes.includes(locale)) {\r\n throw new UnknownLocaleError(locale, localeCodes);\r\n }\r\n\r\n return this.cookie(LOCALE_COOKIE_NAME, locale, { raw: true });\r\n }\r\n\r\n /**\r\n * Clear a cookie from the response\r\n *\r\n * @example\r\n * response.clearCookie('token', { path: '/' });\r\n */\r\n public clearCookie(name: string, options?: CookieSerializeOptions) {\r\n const defaultOptions = config.get(\"http.cookies.options\", {});\r\n this.baseResponse.clearCookie(name, { ...defaultOptions, ...options });\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Clear every cookie NAME the current request sent — best-effort, not\r\n * exhaustive: HTTP gives the server no way to discover a cookie's `Path`\r\n * or `Domain`, only the name, so a cookie originally set on a `path` or\r\n * `domain` other than the one this call targets (the framework default,\r\n * or whatever is passed here / configured via `http.cookies.options`)\r\n * will NOT be deleted, and nothing will report that — the browser just\r\n * silently ignores a `Set-Cookie` whose scope doesn't match. Pass an\r\n * explicit `path` / `domain` for cookies the app knows it owns on a\r\n * non-default scope; call `clearCookie()` per name for anything else.\r\n *\r\n * @example\r\n * response.clearCookies();\r\n * response.clearCookies({ path: '/admin' });\r\n */\r\n public clearCookies(options?: CookieSerializeOptions) {\r\n for (const name of Object.keys(this.request.cookies)) {\r\n this.clearCookie(name, options);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Alias to header method\r\n */\r\n public setHeader(key: string, value: any) {\r\n return this.header(key, value);\r\n }\r\n\r\n /**\r\n * Send an error response with status code 500\r\n */\r\n public serverError(data: any) {\r\n return this.send(data, 500);\r\n }\r\n\r\n /**\r\n * Send a forbidden response with status code 403\r\n */\r\n public forbidden(\r\n data: any = {\r\n error: \"You are not allowed to access this resource, FORBIDDEN\",\r\n },\r\n ) {\r\n return this.send(data, 403);\r\n }\r\n\r\n /**\r\n * Send a service unavailable response with status code 503\r\n */\r\n public serviceUnavailable(data: any) {\r\n return this.send(data, 503);\r\n }\r\n\r\n /**\r\n * Send an unauthorized response with status code 401\r\n */\r\n public unauthorized(\r\n data: any = {\r\n error: \"unauthorized\",\r\n },\r\n ) {\r\n return this.send(data, 401);\r\n }\r\n\r\n /**\r\n * Send a not found response with status code 404\r\n */\r\n public notFound(\r\n data: any = {\r\n error: \"notFound\",\r\n },\r\n ) {\r\n return this.send(data, 404);\r\n }\r\n\r\n /**\r\n * Send a bad request response with status code 400\r\n */\r\n public badRequest(data: any) {\r\n return this.send(data, 400);\r\n }\r\n\r\n /**\r\n * Send a content too large response with status code 413\r\n */\r\n public contentTooLarge(data: any) {\r\n return this.send(data, 413);\r\n }\r\n\r\n /**\r\n * Send a success response with status code 201\r\n */\r\n public successCreate(data: any) {\r\n return this.send(data, 201);\r\n }\r\n\r\n /**\r\n * Send a success response\r\n */\r\n public success(data: any = { success: true }) {\r\n return this.send(data);\r\n }\r\n\r\n /**\r\n * Send a no content response with status code 204\r\n */\r\n public noContent() {\r\n return this.baseResponse.status(204).send();\r\n }\r\n\r\n /**\r\n * Send an accepted response with status code 202\r\n * Used for async operations that have been accepted but not yet processed\r\n */\r\n public accepted(data: any = { message: \"Request accepted for processing\" }) {\r\n return this.send(data, 202);\r\n }\r\n\r\n /**\r\n * Send a conflict response with status code 409\r\n */\r\n public conflict(data: any = { error: \"Resource conflict\" }) {\r\n return this.send(data, 409);\r\n }\r\n\r\n /**\r\n * Send a too many requests response with status code 429\r\n */\r\n public tooManyRequests(data: any) {\r\n return this.send(data, 429);\r\n }\r\n\r\n /**\r\n * Send an unprocessable entity response with status code 422\r\n * Used for semantic validation errors\r\n */\r\n public unprocessableEntity(data: any) {\r\n return this.send(data, 422);\r\n }\r\n\r\n /**\r\n * Apply response options (cache, disposition, etag)\r\n * Shared helper for sendFile and sendBuffer\r\n */\r\n /**\r\n * Build an RFC 6266–safe `Content-Disposition` value for a file name.\r\n *\r\n * HTTP header values must be ISO-8859-1; Node's `setHeader` throws\r\n * `ERR_INVALID_CHAR` on any other byte, so a raw `filename=\"تقرير.pdf\"` would\r\n * crash the response (a 500 the moment the name is non-ASCII). RFC 6266 solves\r\n * this with two parameters emitted together:\r\n * - `filename=\"<ascii>\"` — sanitised ASCII fallback for legacy clients\r\n * - `filename*=UTF-8''<pct>` — RFC 5987 ext-value; modern browsers restore\r\n * the real (e.g. Arabic) name.\r\n */\r\n private contentDisposition(type: \"inline\" | \"attachment\", rawName: string): string {\r\n // Strip CR/LF first so a crafted file name can never inject extra headers.\r\n const name = (rawName || \"file\").replace(/[\\r\\n]/g, \"\");\r\n\r\n // ASCII fallback: replace quote/backslash + every non-printable-ASCII byte\r\n // (covers all multibyte characters) so the quoted-string is always legal.\r\n const ascii =\r\n name\r\n .replace(/[\"\\\\]/g, \"_\")\r\n .replace(/[^\\x20-\\x7E]/g, \"_\")\r\n .trim() || \"file\";\r\n\r\n // Pure-ASCII name → the quoted form is enough; no ext-value needed.\r\n if (!/[^\\x20-\\x7E]/.test(name)) {\r\n return `${type}; filename=\"${ascii}\"`;\r\n }\r\n\r\n // RFC 5987 ext-value. `encodeURIComponent` leaves ' ( ) * unescaped, but they\r\n // are not valid `attr-char`, so percent-encode those too.\r\n const encoded = encodeURIComponent(name).replace(\r\n /['()*]/g,\r\n (char) => \"%\" + char.charCodeAt(0).toString(16).toUpperCase(),\r\n );\r\n\r\n return `${type}; filename=\"${ascii}\"; filename*=UTF-8''${encoded}`;\r\n }\r\n\r\n private applyResponseOptions(options: SendBufferOptions, defaultFilename?: string): boolean {\r\n // Set content type if provided\r\n if (options.contentType) {\r\n this.baseResponse.type(options.contentType);\r\n }\r\n\r\n // Set cache headers if specified\r\n if (options.cacheTime) {\r\n const cacheControl = options.immutable\r\n ? `public, max-age=${options.cacheTime}, immutable`\r\n : `public, max-age=${options.cacheTime}`;\r\n this.header(\"Cache-Control\", cacheControl);\r\n this.header(\"Expires\", new Date(Date.now() + options.cacheTime * 1000).toUTCString());\r\n }\r\n\r\n // Set ETag if provided (for conditional requests)\r\n if (options.etag) {\r\n this.header(\"ETag\", options.etag);\r\n\r\n // Check If-None-Match for conditional request\r\n const ifNoneMatch = this.request.header(\"if-none-match\");\r\n if (ifNoneMatch && ifNoneMatch === options.etag) {\r\n this.log(\"Content not modified (ETag match), sending 304\");\r\n this.baseResponse.status(304).send();\r\n return true; // Indicates 304 was sent\r\n }\r\n }\r\n\r\n // Set Content-Disposition if inline or filename is specified\r\n if (options.inline !== undefined || options.filename) {\r\n const disposition = options.inline ? \"inline\" : \"attachment\";\r\n const filename = options.filename || defaultFilename || \"file\";\r\n this.header(\"Content-Disposition\", this.contentDisposition(disposition, filename));\r\n }\r\n\r\n return false; // No 304 sent\r\n }\r\n\r\n /**\r\n * Send a file as a response\r\n */\r\n public async sendFile(filePath: string | StorageFile, options?: number | SendFileOptions) {\r\n if (filePath instanceof StorageFile) {\r\n filePath = filePath.absolutePath!;\r\n }\r\n\r\n this.log(`Sending file: ${filePath}`);\r\n\r\n // Check if file exists first\r\n if (!(await fileExistsAsync(filePath))) {\r\n return this.notFound({\r\n error: \"File Not Found\",\r\n });\r\n }\r\n\r\n try {\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Get file stats for ETag and Last-Modified\r\n const stats = await fs.promises.stat(filePath);\r\n const lastModified = stats.mtime;\r\n\r\n // Generate ETag based on file size and modification time\r\n const etag = `\"${stats.size}-${stats.mtime.getTime()}\"`;\r\n\r\n // Set Last-Modified header\r\n this.header(\"Last-Modified\", lastModified.toUTCString());\r\n this.header(\"ETag\", etag);\r\n\r\n // Set content type\r\n const contentType = this.getFileContentType(filePath);\r\n this.baseResponse.type(contentType);\r\n\r\n // Apply common response options (cache, disposition)\r\n const defaultFilename = path.basename(filePath);\r\n const sent304 = this.applyResponseOptions({ ...opts, etag, contentType }, defaultFilename);\r\n if (sent304) return this.baseResponse;\r\n\r\n // Check conditional request headers\r\n const ifNoneMatch = this.request.header(\"if-none-match\");\r\n const ifModifiedSince = this.request.header(\"if-modified-since\");\r\n\r\n // Handle If-None-Match (ETag validation)\r\n if (ifNoneMatch && ifNoneMatch === etag) {\r\n this.log(\"File not modified (ETag match), sending 304\");\r\n return this.baseResponse.status(304).send();\r\n }\r\n\r\n // Handle If-Modified-Since (Last-Modified validation)\r\n if (ifModifiedSince) {\r\n const modifiedSinceDate = new Date(ifModifiedSince);\r\n if (lastModified.getTime() <= modifiedSinceDate.getTime()) {\r\n this.log(\"File not modified (Last-Modified check), sending 304\");\r\n return this.baseResponse.status(304).send();\r\n }\r\n }\r\n\r\n // Use streaming for efficient file sending\r\n const stream = fs.createReadStream(filePath);\r\n\r\n // Handle stream errors\r\n stream.on(\"error\", (error) => {\r\n this.log(`Error reading file: ${error.message}`, \"error\");\r\n if (!this.baseResponse.sent) {\r\n this.serverError({\r\n error: \"Error reading file\",\r\n message: error.message,\r\n });\r\n }\r\n });\r\n\r\n // Send the stream (endTime will be set by finish event listener)\r\n return this.baseResponse.send(stream);\r\n } catch (error: any) {\r\n this.log(`Error sending file: ${error.message}`, \"error\");\r\n return this.serverError({\r\n error: \"Error sending file\",\r\n message: error.message,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Send buffer as a response\r\n * Useful for dynamically generated content (e.g., resized images, generated PDFs)\r\n */\r\n public sendBuffer(buffer: Buffer, options?: number | SendBufferOptions) {\r\n this.log(\"Sending buffer\");\r\n\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Apply common response options (cache, disposition, etag)\r\n const sent304 = this.applyResponseOptions(opts);\r\n if (sent304) return this.baseResponse;\r\n\r\n // Note: endTime is set in the main send() method for non-streaming responses\r\n return this.baseResponse.send(buffer);\r\n }\r\n\r\n /**\r\n * Send an Image instance as a response\r\n * Automatically detects image format and sets content type\r\n */\r\n public async sendImage(\r\n image: any, // Type as 'any' to avoid circular dependency with Image class\r\n options?: number | (Omit<SendBufferOptions, \"contentType\"> & { contentType?: string }),\r\n ) {\r\n this.log(\"Sending image\");\r\n\r\n // Normalize options to object format\r\n const opts = typeof options === \"number\" ? { cacheTime: options } : options || {};\r\n\r\n // Get image metadata to determine format\r\n const metadata = await image.metadata();\r\n const format = metadata.format || \"jpeg\";\r\n\r\n // Convert image to buffer\r\n const buffer = await image.toBuffer();\r\n\r\n // Auto-set content type if not provided\r\n const contentType = opts.contentType || `image/${format}`;\r\n\r\n // Auto-generate ETag if not provided\r\n // Format: \"format-widthxheight-size\" (e.g., \"jpeg-800x600-45231\")\r\n // This catches changes in dimensions, quality, filters, and format\r\n if (!opts.etag) {\r\n const width = metadata.width || 0;\r\n const height = metadata.height || 0;\r\n opts.etag = `\"${format}-${width}x${height}-${buffer.length}\"`;\r\n }\r\n\r\n // Apply common response options with auto-detected content type\r\n const sent304 = this.applyResponseOptions({ ...opts, contentType });\r\n if (sent304) return this.baseResponse;\r\n\r\n // Note: endTime is set in the main send() method for non-streaming responses\r\n return this.baseResponse.send(buffer);\r\n }\r\n\r\n /**\r\n * Send file and cache it\r\n * Cache time in seconds\r\n * Cache time will be one year\r\n */\r\n public sendCachedFile(path: string | StorageFile, cacheTime = 31536000) {\r\n return this.sendFile(path, cacheTime);\r\n }\r\n\r\n /**\r\n * Download the given file path\r\n */\r\n public download(path: string, filename?: string) {\r\n return this.downloadFile(path, filename);\r\n }\r\n\r\n /**\r\n * Download the given file path\r\n */\r\n public async downloadFile(filePath: string, filename?: string) {\r\n // Check if file exists first\r\n if (!(await fileExistsAsync(filePath))) {\r\n return this.notFound({\r\n error: \"File Not Found\",\r\n });\r\n }\r\n\r\n try {\r\n if (!filename) {\r\n filename = path.basename(filePath);\r\n }\r\n\r\n this.baseResponse.header(\r\n \"Content-Disposition\",\r\n this.contentDisposition(\"attachment\", filename),\r\n );\r\n\r\n // this.baseResponse.header(\"Content-Type\", this.getFileContentType(filePath));\r\n this.baseResponse.header(\"Content-Type\", \"application/octet-stream\");\r\n\r\n const stream = fs.createReadStream(filePath);\r\n\r\n // Handle stream errors\r\n stream.on(\"error\", (error) => {\r\n this.log(`Error reading file for download: ${error.message}`, \"error\");\r\n if (!this.baseResponse.sent) {\r\n this.serverError({\r\n error: \"Error reading file\",\r\n message: error.message,\r\n });\r\n }\r\n });\r\n\r\n // Send the stream (endTime will be set by finish event listener)\r\n return this.baseResponse.send(stream);\r\n } catch (error: any) {\r\n this.log(`Error downloading file: ${error.message}`, \"error\");\r\n return this.serverError({\r\n error: \"Error downloading file\",\r\n message: error.message,\r\n });\r\n }\r\n }\r\n\r\n /**\r\n * Get content type of the given path\r\n */\r\n public getFileContentType(filePath: string) {\r\n const type = mime.getType(filePath) || \"application/octet-stream\";\r\n return type;\r\n }\r\n\r\n /**\r\n * Mark the response as failed\r\n */\r\n public failedSchema(result: ValidationResult) {\r\n const { errors, inputKey, inputError, status } = config.get(\"validation.response\", {\r\n errors: \"errors\",\r\n inputKey: \"input\",\r\n inputError: \"error\",\r\n status: 422,\r\n });\r\n\r\n log.error(\"request\", \"validation\", `${this.request.id} - Validation failed`);\r\n\r\n return this.send(\r\n {\r\n [errors]: result.errors.map((error) => ({\r\n [inputKey]: error.input,\r\n [inputError]: error.error,\r\n })),\r\n },\r\n status,\r\n );\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../../../../../../../core/src/http/server.ts"],"mappings":";;;KAIY,iBAAA,GAAkB,UAAU,QAAQ,OAAA;AAAA,iBA8ChC,eAAA,
|
|
1
|
+
{"version":3,"file":"server.d.mts","names":[],"sources":["../../../../../../../core/src/http/server.ts"],"mappings":";;;KAIY,iBAAA,GAAkB,UAAU,QAAQ,OAAA;AAAA,iBA8ChC,eAAA,CAAgB,OAAA,GAAU,oBAAA,GAAuB,iBAAe;AA9ChF;;;AAAA,iBA2FgB,aAAA,IAAiB,iBAAe;AA3FO;AA8CvD;;;AA9CuD,KAmG3C,cAAA;EAAmB,KAAA,QAAa,OAAO;AAAA;;;AArD6B;AA6ChF;;;;iBAiBsB,sBAAA,CACpB,MAAA,EAAQ,cAAA,EACR,SAAA,WACC,OAAO"}
|
package/esm/http/server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":[],"sources":["../../../../../../../core/src/http/server.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport Fastify, { type FastifyServerOptions } from \"fastify\";\nimport { normalizeRequestPath } from \"../router/normalize-request-path\";\n\nexport type FastifyInstance = ReturnType<typeof Fastify>;\n\n// Instantiate Fastify server\nlet server: FastifyInstance | undefined = undefined;\n\n/** Fastify's accepted, non-null `trustProxy` configuration shape. */\ntype TrustProxy = NonNullable<FastifyServerOptions[\"trustProxy\"]>;\ntype TrustProxyPredicate = (address: string, hop: number) => boolean;\n\nfunction isTrustProxyPredicate(value: unknown): value is TrustProxyPredicate {\n return typeof value === \"function\";\n}\n\nfunction isTrustProxyList(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every((entry) => typeof entry === \"string\" && entry.trim().length > 0)\n );\n}\n\nfunction resolveTrustProxy(value: unknown): TrustProxy {\n if (value === undefined || value === null) return false;\n\n if (typeof value === \"boolean\" || isTrustProxyPredicate(value)) return value;\n\n if (typeof value === \"string\" && value.trim().length > 0) return value;\n\n if (isTrustProxyList(value)) return value;\n\n const received = Array.isArray(value) ? \"array\" : typeof value;\n\n throw new TypeError(\n \"Invalid http.trustProxy configuration: expected a boolean, a non-empty IP/CIDR string, \" +\n `a non-empty string array, or a predicate function; received ${received}.`,\n );\n}\n\nfunction normalizeRequestUrl(url: string): string {\n const queryIndex = url.indexOf(\"?\");\n\n if (queryIndex === -1) return normalizeRequestPath(url);\n\n return normalizeRequestPath(url.slice(0, queryIndex)) + url.slice(queryIndex);\n}\n\nexport function startHttpServer(
|
|
1
|
+
{"version":3,"file":"server.mjs","names":[],"sources":["../../../../../../../core/src/http/server.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport Fastify, { type FastifyServerOptions } from \"fastify\";\nimport { normalizeRequestPath } from \"../router/normalize-request-path\";\n\nexport type FastifyInstance = ReturnType<typeof Fastify>;\n\n// Instantiate Fastify server\nlet server: FastifyInstance | undefined = undefined;\n\n/** Fastify's accepted, non-null `trustProxy` configuration shape. */\ntype TrustProxy = NonNullable<FastifyServerOptions[\"trustProxy\"]>;\ntype TrustProxyPredicate = (address: string, hop: number) => boolean;\n\nfunction isTrustProxyPredicate(value: unknown): value is TrustProxyPredicate {\n return typeof value === \"function\";\n}\n\nfunction isTrustProxyList(value: unknown): value is string[] {\n return (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every((entry) => typeof entry === \"string\" && entry.trim().length > 0)\n );\n}\n\nfunction resolveTrustProxy(value: unknown): TrustProxy {\n if (value === undefined || value === null) return false;\n\n if (typeof value === \"boolean\" || isTrustProxyPredicate(value)) return value;\n\n if (typeof value === \"string\" && value.trim().length > 0) return value;\n\n if (isTrustProxyList(value)) return value;\n\n const received = Array.isArray(value) ? \"array\" : typeof value;\n\n throw new TypeError(\n \"Invalid http.trustProxy configuration: expected a boolean, a non-empty IP/CIDR string, \" +\n `a non-empty string array, or a predicate function; received ${received}.`,\n );\n}\n\nfunction normalizeRequestUrl(url: string): string {\n const queryIndex = url.indexOf(\"?\");\n\n if (queryIndex === -1) return normalizeRequestPath(url);\n\n return normalizeRequestPath(url.slice(0, queryIndex)) + url.slice(queryIndex);\n}\n\nexport function startHttpServer(options?: FastifyServerOptions): FastifyInstance {\n // `config.set(key, undefined)` stores null rather than unsetting, so an app\n // that clears a key is treated the same as an app that never configured it.\n const trustProxy = resolveTrustProxy(config.get(\"http.trustProxy\", false));\n\n const bodyLimit = config.get(\"http.bodyLimit\") ?? undefined;\n const configuredRewriteUrl = options?.rewriteUrl;\n\n return (server = Fastify({\n // `X-Forwarded-For` is client-settable and spoofable, and `request.ip` is\n // what @fastify/rate-limit keys its buckets on — so trusting it by default\n // makes rate limiting bypassable on any deployment NOT behind a proxy that\n // strips the header. Apps behind such a proxy opt in explicitly.\n //\n // Validated shapes resolve exactly as Fastify does: `true`, a CIDR/IP list\n // (string, comma-separated string or array), or a predicate.\n trustProxy,\n // No default: an app that configures nothing keeps Fastify's own 1MB limit\n // rather than the historical 200GB, which silently removed the protection\n // Fastify provides. Per-route caps go through `serverOptions.bodyLimit`;\n // the `maxBodySize()` middleware runs AFTER parsing and cannot reject\n // before the bytes are resident.\n ...(bodyLimit !== undefined && { bodyLimit }),\n // Close idle keep-alive connections on shutdown while letting in-flight\n // requests finish — the basis for graceful draining. Override via\n // `http.gracefulShutdown.forceCloseConnections`.\n forceCloseConnections: config.get(\"http.gracefulShutdown.forceCloseConnections\", \"idle\"),\n ...options,\n // Fastify calls this before route matching in both production `scan()` and\n // the development wildcard dispatcher. Compose an app-supplied rewrite\n // first, then apply the one framework request-path normalizer so the two\n // modes cannot disagree about a terminal slash.\n rewriteUrl(request) {\n const rewritten = configuredRewriteUrl\n ? configuredRewriteUrl.call(this, request)\n : (request.url ?? \"/\");\n\n return normalizeRequestUrl(rewritten);\n },\n }));\n}\n\n/**\n * Expose the server to be publicly accessible\n */\nexport function getHttpServer(): FastifyInstance {\n return server;\n}\n\n/**\n * Minimal shape needed to close a server — lets {@link closeServerWithTimeout}\n * be unit-tested with a fake instead of a real Fastify instance.\n */\nexport type ClosableServer = { close: () => Promise<unknown> };\n\n/**\n * Close a server, bounded by a timeout. Fastify's `close()` stops accepting new\n * requests (it answers 503 while closing) and drains the in-flight ones; this\n * wraps it so a single stuck request can't hang shutdown forever.\n *\n * @returns `true` if the server drained cleanly, `false` if the timeout fired first.\n */\nexport async function closeServerWithTimeout(\n server: ClosableServer,\n timeoutMs: number,\n): Promise<boolean> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const drained = server.close().then(() => true);\n\n const timedOut = new Promise<boolean>((resolve) => {\n timer = setTimeout(() => resolve(false), timeoutMs);\n });\n\n try {\n return await Promise.race([drained, timedOut]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n"],"mappings":";;;;;AAOA,IAAI,SAAsC;AAM1C,SAAS,sBAAsB,OAA8C;CAC3E,OAAO,OAAO,UAAU;AAC1B;AAEA,SAAS,iBAAiB,OAAmC;CAC3D,OACE,MAAM,QAAQ,KAAK,KACnB,MAAM,SAAS,KACf,MAAM,OAAO,UAAU,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,CAAC;AAE/E;AAEA,SAAS,kBAAkB,OAA4B;CACrD,IAAI,UAAU,UAAa,UAAU,MAAM,OAAO;CAElD,IAAI,OAAO,UAAU,aAAa,sBAAsB,KAAK,GAAG,OAAO;CAEvE,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,GAAG,OAAO;CAEjE,IAAI,iBAAiB,KAAK,GAAG,OAAO;CAIpC,MAAM,IAAI,UACR,sJAHe,MAAM,QAAQ,KAAK,IAAI,UAAU,OAAO,MAImB,EAC5E;AACF;AAEA,SAAS,oBAAoB,KAAqB;CAChD,MAAM,aAAa,IAAI,QAAQ,GAAG;CAElC,IAAI,eAAe,IAAI,OAAO,qBAAqB,GAAG;CAEtD,OAAO,qBAAqB,IAAI,MAAM,GAAG,UAAU,CAAC,IAAI,IAAI,MAAM,UAAU;AAC9E;AAEA,SAAgB,gBAAgB,SAAiD;CAG/E,MAAM,aAAa,kBAAkB,OAAO,IAAI,mBAAmB,KAAK,CAAC;CAEzE,MAAM,YAAY,OAAO,IAAI,gBAAgB,KAAK;CAClD,MAAM,uBAAuB,SAAS;CAEtC,OAAQ,SAAS,QAAQ;EAQvB;EAMA,GAAI,cAAc,UAAa,EAAE,UAAU;EAI3C,uBAAuB,OAAO,IAAI,+CAA+C,MAAM;EACvF,GAAG;EAKH,WAAW,SAAS;GAKlB,OAAO,oBAJW,uBACd,qBAAqB,KAAK,MAAM,OAAO,IACtC,QAAQ,OAAO,GAEgB;EACtC;CACF,CAAC;AACH;;;;AAKA,SAAgB,gBAAiC;CAC/C,OAAO;AACT;;;;;;;;AAeA,eAAsB,uBACpB,QACA,WACkB;CAClB,IAAI;CAEJ,MAAM,UAAU,OAAO,MAAM,CAAC,CAAC,WAAW,IAAI;CAE9C,MAAM,WAAW,IAAI,SAAkB,YAAY;EACjD,QAAQ,iBAAiB,QAAQ,KAAK,GAAG,SAAS;CACpD,CAAC;CAED,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CAAC,SAAS,QAAQ,CAAC;CAC/C,UAAU;EACR,IAAI,OACF,aAAa,KAAK;CAEtB;AACF"}
|
package/esm/http/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/http/types.ts"],"mappings":";;;;;;;KAMY,YAAA;;;AAAZ;;;;AAAwB;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/http/types.ts"],"mappings":";;;;;;;KAMY,YAAA;;;AAAZ;;;;AAAwB;AA0BxB;;;;AAQa;AAoBb;;;;AAA4B;AAc5B;;;;AACU;AAMV;UAjDiB,aAAA;;;;;;;;EAQf,WAAW;AAAA;;;;;;;;;;AAiEF;AAKX;;;;AAAyB;AAiEzB;;UAnHiB,WAAA;;;;;;AAkIO;AAMxB;;;;;;KA1HY,kBAAA;EACV,QAAQ;AAAA;;;;KAME,gBAAA;;;;AAIR;;;;EAIA,OAAA,CAAQ,QAAA;;;;EAIR,MAAA;;;;EAIA,OAAA,CAAQ,MAAA;;;;;;;;EAQR,OAAA;;;;KAKQ,aAAA;;;;;;;;;;;;;;;AAgTkB;;;;;;;;;;;;;;AAoBvB;AAGP;;AAHO;;;;;;;;;;;;;;;;;;;;;;AA2BA;;;;;;;;;;UA9RU,iBAAA;;;;;EAKf,MAAA;;;;;;EAMA,WAAA;;;;EAIA,UAAA,EAAY,UAAU;AAAA;;;;UAMP,kBAAA;;;;EAIf,IAAA;;;;EAIA,GAAA;;;;EAIA,IAAA,GAAO,kBAAA;;;;;;EAMP,eAAA;;;;;;;;;;EAUA,SAAA;;;;;;;;;;;;;;;;;;;;EAoBA,UAAA,mCAA6C,OAAA,UAAiB,GAAA;EAC9D,OAAA;;;;IAIE,MAAA;;;;IAIA,OAAA,GAAU,sBAAA;EAAA;;;;EAKZ,SAAA;;;;;;IAME,GAAA;;;;;;IAMA,QAAA;EAAA;;;;;;;;EASF,SAAA;;;;;;IAME,MAAA;;;;IAIA,SAAA;;;;;;;IAOA,OAAA;EAAA;;;;EAKF,WAAA;;;;;;IAME,GAAA;;;;;;IAMA,UAAA;;;;;;;IAOA,OAAA;;;;IAIA,MAAA;EAAA;;;;EAKF,WAAA;;;;;;;IAOE,OAAA;;;;;;IAMA,SAAA;;;;;;IAMA,UAAA;EAAA;;;;EAKF,gBAAA;;;;;;;IAOE,OAAA;;;;;;;;IAQA,qBAAA;EAAA;;;;EAKF,MAAA;;;;;;IAME,OAAA;;;;;;;IAOA,IAAA;;;;;;;IAOA,aAAA;EAAA;;;;EAKF,IAAA;;;;EAIA,UAAA;;;;IAIE,GAAA,GAAM,UAAA;;;;IAIN,IAAA,GAAO,iBAAA;;;;IAIP,MAAA,GAAS,iBAAA;EAAA;AAAA;AAAA,KAID,wBAAA;;;;EAIV,IAAA,GAAO,IAAA;;;;EAIP,MAAA,GAAS,IAAA,EAAM,KAAA,CAAM,SAAS;;;;EAI9B,GAAA;;;;EAIA,KAAA;AAAA;AAAA,KAGU,qBAAA;;;;;;;EAOV,IAAA,GAAO,KAAA,UAAe,IAAA,OAAW,EAAA,cAAgB,qBAAA;;;;EAIjD,OAAA,GAAU,OAAA,aAAoB,qBAAA;;;;EAI9B,GAAA,QAAW,qBAAA;;;;;EAKX,YAAA,GAAe,OAAA,iBAAwB,qBAAA;;;;EAIvC,KAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.mts","names":[],"sources":["../../../../../../../core/src/image/image.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"image.d.mts","names":[],"sources":["../../../../../../../core/src/image/image.ts"],"mappings":"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"}
|
package/esm/image/image.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.mjs","names":[],"sources":["../../../../../../../core/src/image/image.ts"],"sourcesContent":["import { http } from \"@mongez/http\";\r\nimport { createRequire } from \"node:module\";\r\nimport type sharp from \"sharp\";\r\nimport type { FormatEnum } from \"sharp\";\r\n\r\n// ============================================================\r\n// Lazily Resolved Sharp Module\r\n// ============================================================\r\n\r\n/**\r\n * Installation instructions for sharp\r\n */\r\nconst SHARP_INSTALL_INSTRUCTIONS = `\r\nImage processing requires the sharp package.\r\nInstall it with:\r\n\r\n warlock add image\r\n\r\nOr manually:\r\n\r\n npm install sharp\r\n pnpm add sharp\r\n yarn add sharp\r\n`.trim();\r\n\r\n/**\r\n * Cached sharp function, populated by the first `resolveSharp()` call\r\n */\r\nlet sharpFn: typeof sharp | null = null;\r\n\r\n/**\r\n * Whether the resolution attempt already ran (success or failure)\r\n */\r\nlet sharpResolved = false;\r\n\r\n/**\r\n * Why the resolution failed, when it failed for a reason other than absence.\r\n *\r\n * Cached and re-thrown on every later call: the resolution attempt runs exactly\r\n * once, so without this the second construction would fall through to the\r\n * \"not installed\" branch and report a different, wrong cause.\r\n */\r\nlet sharpLoadError: Error | null = null;\r\n\r\n/**\r\n * `new Error(message, { cause })` is ES2022, and this package still compiles\r\n * against the ES2020 lib (see #16), where `Error` is typed as taking a message\r\n * only. Node has supported the option since v16, so this is the type layer\r\n * catching up with the runtime, not a change in what the code does.\r\n */\r\ntype ErrorWithCauseConstructor = new (message: string, options: { cause: unknown }) => Error;\r\n\r\nconst ErrorWithCause = Error as ErrorWithCauseConstructor;\r\n\r\n/**\r\n * Whether `error` says sharp itself is absent, as opposed to present but broken.\r\n *\r\n * Both halves are load-bearing. `MODULE_NOT_FOUND` alone is not enough: a\r\n * dependency missing *inside* sharp raises the very same code (`Cannot find\r\n * module 'color'`), and treating that as absence would tell the operator to\r\n * install a package they already have. So the message has to name the specifier\r\n * `'sharp'` exactly — quoted, which is also what keeps `'sharp-cli'` and friends\r\n * from matching.\r\n */\r\nfunction isSharpMissing(error: unknown): boolean {\r\n if (!(error instanceof Error)) return false;\r\n\r\n return (\r\n (error as NodeJS.ErrnoException).code === \"MODULE_NOT_FOUND\" &&\r\n error.message.includes(\"Cannot find module 'sharp'\")\r\n );\r\n}\r\n\r\n/**\r\n * Resolve sharp synchronously, on first use.\r\n *\r\n * Resolution is deliberately *lazy* and *synchronous*:\r\n *\r\n * - Lazy, because a top-level require would drag sharp's native binary into\r\n * every `import \"@warlock.js/core\"`, including apps that never touch images.\r\n * - Synchronous, because the `Image` constructor is synchronous. An async\r\n * import kicked off at module load leaves a window in which the module is\r\n * neither loaded nor known to be missing, and a constructor running inside\r\n * that window has no correct answer to give.\r\n *\r\n * `createRequire` gives an ESM-safe `require`, and the outcome — module, absence\r\n * or load failure — is cached, so the resolution runs exactly once per process.\r\n * The failure *reason* is cached too, not just the fact of it: every call after\r\n * a failed load has to report the same cause as the first one.\r\n *\r\n * @throws when sharp is not installed, or is installed but cannot be loaded\r\n */\r\nfunction resolveSharp(): typeof sharp {\r\n if (!sharpResolved) {\r\n sharpResolved = true;\r\n\r\n try {\r\n const require = createRequire(import.meta.url);\r\n const module = require(\"sharp\");\r\n sharpFn = (module.default ?? module) as typeof sharp;\r\n } catch (error) {\r\n sharpFn = null;\r\n\r\n if (!isSharpMissing(error)) {\r\n // sharp is there, it just would not load — almost always a binary built\r\n // for another platform. Its own error names the runtime and the fix, so\r\n // it is inlined *and* chained: a terminal that never prints `cause`\r\n // must still show the text that actually helps.\r\n sharpLoadError = new ErrorWithCause(\r\n `Failed to load \"sharp\": ${(error as Error).message}`,\r\n { cause: error },\r\n );\r\n }\r\n }\r\n }\r\n\r\n if (sharpLoadError) {\r\n throw sharpLoadError;\r\n }\r\n\r\n if (!sharpFn) {\r\n throw new Error(`sharp is not installed.\\n\\n${SHARP_INSTALL_INSTRUCTIONS}`);\r\n }\r\n\r\n return sharpFn;\r\n}\r\n\r\n// ============================================================\r\n// Types\r\n// ============================================================\r\n\r\nexport type ImageFormat = keyof FormatEnum;\r\n\r\nexport type ImageInput = string | Buffer | Uint8Array | ArrayBuffer;\r\n\r\n/**\r\n * Watermark configuration for deferred execution\r\n */\r\nexport type WatermarkConfig = {\r\n image: ImageInput | Image;\r\n options: sharp.OverlayOptions;\r\n};\r\n\r\n/**\r\n * Operation descriptor for deferred pipeline execution.\r\n * All operations are stored and executed at save/toBuffer time.\r\n */\r\ntype ImageOperation =\r\n | { type: \"resize\"; options: sharp.ResizeOptions }\r\n | { type: \"crop\"; options: sharp.Region }\r\n | { type: \"rotate\"; angle: number }\r\n | { type: \"flip\" }\r\n | { type: \"flop\" }\r\n | { type: \"blur\"; sigma: number }\r\n | { type: \"sharpen\"; options?: sharp.SharpenOptions }\r\n | { type: \"blackAndWhite\" }\r\n | { type: \"opacity\"; value: number }\r\n | { type: \"negate\"; options?: sharp.NegateOptions }\r\n | { type: \"tint\"; color: sharp.Color }\r\n | { type: \"trim\"; options?: sharp.TrimOptions }\r\n | { type: \"watermark\"; config: WatermarkConfig }\r\n | { type: \"watermarks\"; configs: WatermarkConfig[] };\r\n\r\n/**\r\n * Transformation options that can be applied in batch via `apply()` method.\r\n *\r\n * **Execution Order (when using apply()):**\r\n * 1. resize - Resize first to work with correct dimensions\r\n * 2. crop - Crop after resize to extract the desired region\r\n * 3. rotate - Rotation after sizing\r\n * 4. flip/flop - Mirror operations\r\n * 5. blackAndWhite/grayscale - Color space conversion\r\n * 6. blur - Blur effect\r\n * 7. sharpen - Sharpen effect\r\n * 8. tint - Color overlay\r\n * 9. negate - Invert colors\r\n * 10. opacity - Transparency (applied via composite)\r\n * 11. format/quality - Applied on save/export\r\n */\r\nexport type ImageTransformOptions = {\r\n /**\r\n * Output quality (1-100), applied based on final format\r\n */\r\n quality?: number;\r\n /**\r\n * Output format (jpeg, png, webp, avif, etc.)\r\n */\r\n format?: ImageFormat;\r\n /**\r\n * Resize options\r\n */\r\n resize?: sharp.ResizeOptions;\r\n /**\r\n * Crop/extract region\r\n */\r\n crop?: sharp.Region;\r\n /**\r\n * Rotation angle in degrees\r\n */\r\n rotate?: number;\r\n /**\r\n * Flip vertically (top to bottom)\r\n */\r\n flip?: boolean;\r\n /**\r\n * Flop horizontally (left to right)\r\n */\r\n flop?: boolean;\r\n /**\r\n * Convert to black and white\r\n */\r\n blackAndWhite?: boolean;\r\n /**\r\n * Alias for blackAndWhite\r\n */\r\n grayscale?: boolean;\r\n /**\r\n * Blur sigma (must be >= 0.3)\r\n */\r\n blur?: number;\r\n /**\r\n * Sharpen options\r\n */\r\n sharpen?: sharp.SharpenOptions | boolean;\r\n /**\r\n * Tint color\r\n */\r\n tint?: sharp.Color;\r\n /**\r\n * Negate/invert colors\r\n */\r\n negate?: sharp.NegateOptions | boolean;\r\n /**\r\n * Opacity (0-100)\r\n */\r\n opacity?: number;\r\n /**\r\n * Trim options\r\n */\r\n trim?: sharp.TrimOptions | boolean;\r\n /**\r\n * Single watermark\r\n */\r\n watermark?: WatermarkConfig;\r\n /**\r\n * Multiple watermarks\r\n */\r\n watermarks?: WatermarkConfig[];\r\n};\r\n\r\n/**\r\n * Internal options stored for deferred application\r\n */\r\ntype InternalOptions = {\r\n quality?: number;\r\n format?: ImageFormat;\r\n};\r\n\r\n/**\r\n * Image manipulation class with deferred pipeline execution.\r\n *\r\n * **Important:** This class requires the `sharp` package to be installed.\r\n * Install it with: `warlock add image` or `npm install sharp`\r\n *\r\n * Sharp is resolved synchronously on the first construction that needs it, so\r\n * the constructor and all chainable methods remain synchronous, and a missing\r\n * sharp throws at construction rather than at some later await.\r\n *\r\n * All operations are synchronous and stored as descriptors.\r\n * The pipeline is executed only when calling output methods:\r\n * - `save()` - Save to file\r\n * - `toBuffer()` - Get as buffer\r\n * - `toBase64()` - Get as base64 string\r\n * - `toDataUrl()` - Get as data URL\r\n *\r\n * @example\r\n * ```typescript\r\n * // All chaining is synchronous - single await at the end\r\n * await new Image(\"photo.jpg\")\r\n * .resize({ width: 800 })\r\n * .watermark(\"logo.png\", { gravity: \"southeast\" })\r\n * .quality(85)\r\n * .save(\"output.jpg\");\r\n * ```\r\n */\r\nexport class Image {\r\n /**\r\n * Image options that will be applied on save/export\r\n */\r\n protected options: InternalOptions = {};\r\n\r\n /**\r\n * Deferred operations pipeline\r\n */\r\n protected operations: ImageOperation[] = [];\r\n\r\n /**\r\n * Cached metadata to avoid repeated async calls\r\n */\r\n protected cachedMetadata: sharp.Metadata | null = null;\r\n\r\n /**\r\n * Whether the pipeline has been executed\r\n */\r\n protected pipelineExecuted = false;\r\n\r\n /**\r\n * Sharp image object\r\n */\r\n public readonly image: sharp.Sharp;\r\n\r\n /**\r\n * Formats that support quality option\r\n */\r\n protected static readonly QUALITY_FORMATS = [\"jpeg\", \"jpg\", \"webp\", \"avif\", \"tiff\", \"heif\"];\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(image: ImageInput | sharp.Sharp) {\r\n // An existing sharp instance carries its own module — short-circuit before\r\n // resolving sharp, so cloning never pays for (or fails on) a module load.\r\n if (image instanceof Object && \"clone\" in image && typeof image.clone === \"function\") {\r\n this.image = image as sharp.Sharp;\r\n } else {\r\n this.image = resolveSharp()(image as ImageInput);\r\n }\r\n }\r\n\r\n /**\r\n * Create image instance from file path\r\n */\r\n public static fromFile(path: string): Image {\r\n return new Image(path);\r\n }\r\n\r\n /**\r\n * Create image instance from buffer\r\n */\r\n public static fromBuffer(buffer: Buffer): Image {\r\n return new Image(buffer);\r\n }\r\n\r\n /**\r\n * Create image instance from url\r\n */\r\n public static async fromUrl(url: string): Promise<Image> {\r\n const { data, error } = await http.get<ArrayBuffer>(url, {\r\n responseType: \"arrayBuffer\",\r\n });\r\n\r\n if (error || !data) {\r\n throw new Error(\r\n `Failed to load image from URL \"${url}\": ${error?.message ?? \"Empty response received\"}`,\r\n );\r\n }\r\n\r\n return new Image(Buffer.from(data));\r\n }\r\n\r\n /**\r\n * Add an operation to the deferred pipeline\r\n */\r\n protected addOperation(operation: ImageOperation): this {\r\n this.operations.push(operation);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Apply multiple transformations at once with a predefined execution order.\r\n *\r\n * This method ensures transformations are applied in a logical order:\r\n * resize → crop → rotate → flip/flop → colorspace → effects → opacity → format\r\n *\r\n * For custom ordering, use individual chained methods instead.\r\n */\r\n public apply(options: ImageTransformOptions): this {\r\n // 1. Resize first to work with correct dimensions\r\n if (options.resize) {\r\n this.resize(options.resize);\r\n }\r\n\r\n // 2. Crop after resize\r\n if (options.crop) {\r\n this.crop(options.crop);\r\n }\r\n\r\n // 3. Rotation\r\n if (options.rotate !== undefined) {\r\n this.rotate(options.rotate);\r\n }\r\n\r\n // 4. Mirror operations\r\n if (options.flip) {\r\n this.flip();\r\n }\r\n\r\n if (options.flop) {\r\n this.flop();\r\n }\r\n\r\n // 5. Color space conversion\r\n if (options.blackAndWhite || options.grayscale) {\r\n this.blackAndWhite();\r\n }\r\n\r\n // 6. Blur effect\r\n if (options.blur !== undefined) {\r\n this.blur(options.blur);\r\n }\r\n\r\n // 7. Sharpen effect\r\n if (options.sharpen) {\r\n const sharpenOptions = typeof options.sharpen === \"boolean\" ? undefined : options.sharpen;\r\n this.sharpen(sharpenOptions);\r\n }\r\n\r\n // 8. Tint color\r\n if (options.tint) {\r\n this.tint(options.tint);\r\n }\r\n\r\n // 9. Negate/invert\r\n if (options.negate) {\r\n const negateOptions = typeof options.negate === \"boolean\" ? undefined : options.negate;\r\n this.negate(negateOptions);\r\n }\r\n\r\n // 10. Trim edges\r\n if (options.trim) {\r\n const trimOptions = typeof options.trim === \"boolean\" ? undefined : options.trim;\r\n this.trim(trimOptions);\r\n }\r\n\r\n // 11. Watermarks\r\n if (options.watermark) {\r\n this.watermark(options.watermark.image, options.watermark.options);\r\n }\r\n\r\n if (options.watermarks) {\r\n this.watermarks(options.watermarks);\r\n }\r\n\r\n // 12. Opacity (applied via composite)\r\n if (options.opacity !== undefined) {\r\n this.opacity(options.opacity);\r\n }\r\n\r\n // 13. Store format/quality for deferred application\r\n if (options.format) {\r\n this.format(options.format);\r\n }\r\n\r\n if (options.quality !== undefined) {\r\n this.quality(options.quality);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set image opacity (0-100)\r\n */\r\n public opacity(value: number): this {\r\n if (value < 0 || value > 100) {\r\n throw new Error(\"Opacity must be between 0 and 100\");\r\n }\r\n\r\n return this.addOperation({ type: \"opacity\", value });\r\n }\r\n\r\n /**\r\n * Convert image to black and white\r\n */\r\n public blackAndWhite(): this {\r\n return this.addOperation({ type: \"blackAndWhite\" });\r\n }\r\n\r\n /**\r\n * Alias for blackAndWhite\r\n */\r\n public grayscale(): this {\r\n return this.blackAndWhite();\r\n }\r\n\r\n /**\r\n * Get image dimensions (cached after first call)\r\n */\r\n public async dimensions(): Promise<{\r\n width: number | undefined;\r\n height: number | undefined;\r\n }> {\r\n const metadata = await this.metadata();\r\n\r\n return { width: metadata.width, height: metadata.height };\r\n }\r\n\r\n /**\r\n * Get image metadata (cached after first call)\r\n *\r\n * The metadata is cached to avoid repeated async operations.\r\n * Use `refreshMetadata()` to force a fresh fetch.\r\n */\r\n public async metadata(): Promise<sharp.Metadata> {\r\n if (!this.cachedMetadata) {\r\n this.cachedMetadata = await this.image.metadata();\r\n }\r\n\r\n return this.cachedMetadata;\r\n }\r\n\r\n /**\r\n * Force refresh of cached metadata\r\n *\r\n * Call this after transformations if you need updated metadata.\r\n */\r\n public async refreshMetadata(): Promise<sharp.Metadata> {\r\n this.cachedMetadata = await this.image.metadata();\r\n\r\n return this.cachedMetadata;\r\n }\r\n\r\n /**\r\n * Clear cached metadata\r\n */\r\n public clearMetadataCache(): this {\r\n this.cachedMetadata = null;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Resize image\r\n */\r\n public resize(options: sharp.ResizeOptions): this {\r\n if (typeof options.width !== \"undefined\" && !options.width) {\r\n delete options.width;\r\n }\r\n\r\n if (typeof options.height !== \"undefined\" && !options.height) {\r\n delete options.height;\r\n }\r\n\r\n return this.addOperation({ type: \"resize\", options });\r\n }\r\n\r\n /**\r\n * Crop/extract a region from the image\r\n */\r\n public crop(options: sharp.Region): this {\r\n return this.addOperation({ type: \"crop\", options });\r\n }\r\n\r\n /**\r\n * Set image quality (1-100)\r\n * Quality is stored and applied when saving/exporting\r\n * based on the final format.\r\n */\r\n public quality(quality: number): this {\r\n if (quality < 1 || quality > 100) {\r\n throw new Error(\"Quality must be between 1 and 100\");\r\n }\r\n\r\n this.options.quality = quality;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Execute the deferred pipeline - apply all stored operations\r\n */\r\n protected async executePipeline(): Promise<sharp.Sharp> {\r\n if (this.pipelineExecuted) {\r\n return this.image;\r\n }\r\n\r\n for (const operation of this.operations) {\r\n await this.executeOperation(this.image, operation);\r\n }\r\n\r\n await this.applyFormatAndQuality(this.image);\r\n\r\n this.pipelineExecuted = true;\r\n\r\n return this.image;\r\n }\r\n\r\n /**\r\n * Execute a single operation\r\n */\r\n protected async executeOperation(image: sharp.Sharp, operation: ImageOperation): Promise<void> {\r\n switch (operation.type) {\r\n case \"resize\":\r\n image.resize(operation.options);\r\n break;\r\n\r\n case \"crop\":\r\n image.extract(operation.options);\r\n break;\r\n\r\n case \"rotate\":\r\n image.rotate(operation.angle);\r\n break;\r\n\r\n case \"flip\":\r\n image.flip();\r\n break;\r\n\r\n case \"flop\":\r\n image.flop();\r\n break;\r\n\r\n case \"blur\":\r\n image.blur(operation.sigma);\r\n break;\r\n\r\n case \"sharpen\":\r\n image.sharpen(operation.options);\r\n break;\r\n\r\n case \"blackAndWhite\":\r\n image.toColourspace(\"b-w\");\r\n break;\r\n\r\n case \"opacity\": {\r\n const alpha = Math.round((operation.value / 100) * 255);\r\n const alphaPixel = Buffer.from([255, 255, 255, alpha]);\r\n image.composite([\r\n {\r\n blend: \"dest-in\",\r\n input: alphaPixel,\r\n },\r\n ]);\r\n break;\r\n }\r\n\r\n case \"negate\":\r\n image.negate(operation.options);\r\n break;\r\n\r\n case \"tint\":\r\n image.tint(operation.color);\r\n break;\r\n\r\n case \"trim\":\r\n image.trim(operation.options);\r\n break;\r\n\r\n case \"watermark\": {\r\n const buffer = await this.resolveImageBuffer(operation.config.image);\r\n image.composite([\r\n {\r\n input: buffer,\r\n ...operation.config.options,\r\n },\r\n ]);\r\n break;\r\n }\r\n\r\n case \"watermarks\": {\r\n const buffers = await Promise.all(\r\n operation.configs.map((config) => this.resolveImageBuffer(config.image)),\r\n );\r\n image.composite(\r\n operation.configs.map((config, index) => ({\r\n input: buffers[index],\r\n ...config.options,\r\n })),\r\n );\r\n break;\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Resolve an image input to a buffer\r\n */\r\n protected async resolveImageBuffer(input: ImageInput | Image): Promise<Buffer> {\r\n if (input instanceof Image) {\r\n // For Image instances, get buffer without applying options (raw buffer)\r\n return input.image.toBuffer();\r\n }\r\n\r\n // For other inputs (path, buffer, etc.), create temp sharp instance\r\n const tempImage = resolveSharp()(input);\r\n\r\n return tempImage.toBuffer();\r\n }\r\n\r\n /**\r\n * Apply format and quality options.\r\n * If no format is explicitly set, preserves the original format and applies\r\n * quality appropriately based on the format type.\r\n */\r\n protected async applyFormatAndQuality(image: sharp.Sharp): Promise<void> {\r\n const { quality, format } = this.options;\r\n\r\n if (format) {\r\n // Explicit format specified\r\n const formatOptions = quality ? { quality } : undefined;\r\n image.toFormat(format, formatOptions);\r\n return;\r\n }\r\n\r\n if (quality === undefined) {\r\n // No quality or format set, nothing to apply\r\n return;\r\n }\r\n\r\n // Quality is set but no format specified - detect original format\r\n const metadata = await this.metadata();\r\n const originalFormat = metadata.format;\r\n\r\n if (!originalFormat) {\r\n // Cannot detect format, default to webp with quality\r\n image.webp({ quality });\r\n return;\r\n }\r\n\r\n // Apply quality based on original format\r\n if (Image.QUALITY_FORMATS.includes(originalFormat)) {\r\n // Format supports quality option\r\n image.toFormat(originalFormat as ImageFormat, { quality });\r\n } else if (originalFormat === \"png\") {\r\n // PNG uses compressionLevel (0-9) instead of quality\r\n // Map quality 1-100 to compressionLevel 9-0 (higher quality = lower compression)\r\n const compressionLevel = Math.round(9 - (quality / 100) * 9);\r\n image.png({ compressionLevel });\r\n } else if (originalFormat === \"gif\") {\r\n // GIF doesn't support quality, just preserve format\r\n image.gif();\r\n }\r\n // Unknown format: preserve as-is (no quality applied)\r\n }\r\n\r\n /**\r\n * Save to file\r\n */\r\n public async save(path: string): Promise<sharp.OutputInfo> {\r\n const image = await this.executePipeline();\r\n\r\n return image.toFile(path);\r\n }\r\n\r\n /**\r\n * Convert to webp and save to file\r\n */\r\n public async saveAsWebp(path: string): Promise<sharp.OutputInfo> {\r\n // Override format to webp\r\n this.options.format = \"webp\";\r\n const image = await this.executePipeline();\r\n\r\n return image.toFile(path);\r\n }\r\n\r\n /**\r\n * Change the file format\r\n */\r\n public format(format: ImageFormat): this {\r\n this.options.format = format;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Add watermark (deferred - executed at save time)\r\n */\r\n public watermark(image: ImageInput | Image, options: sharp.OverlayOptions = {}): this {\r\n return this.addOperation({\r\n type: \"watermark\",\r\n config: { image, options },\r\n });\r\n }\r\n\r\n /**\r\n * Add multiple watermarks (deferred - executed at save time)\r\n */\r\n public watermarks(configs: WatermarkConfig[]): this {\r\n return this.addOperation({\r\n type: \"watermarks\",\r\n configs,\r\n });\r\n }\r\n\r\n /**\r\n * Rotate image\r\n */\r\n public rotate(angle: number): this {\r\n return this.addOperation({ type: \"rotate\", angle });\r\n }\r\n\r\n /**\r\n * Flip image vertically (top to bottom)\r\n */\r\n public flip(): this {\r\n return this.addOperation({ type: \"flip\" });\r\n }\r\n\r\n /**\r\n * Flop image horizontally (left to right)\r\n */\r\n public flop(): this {\r\n return this.addOperation({ type: \"flop\" });\r\n }\r\n\r\n /**\r\n * Blur image\r\n */\r\n public blur(sigma: number): this {\r\n if (sigma < 0.3) {\r\n throw new Error(\"Blur sigma must be at least 0.3\");\r\n }\r\n\r\n return this.addOperation({ type: \"blur\", sigma });\r\n }\r\n\r\n /**\r\n * Convert to base64\r\n */\r\n public async toBase64(): Promise<string> {\r\n const image = await this.executePipeline();\r\n const buffer = await image.toBuffer();\r\n\r\n return buffer.toString(\"base64\");\r\n }\r\n\r\n /**\r\n * Convert to data URL (base64 with mime type prefix)\r\n */\r\n public async toDataUrl(): Promise<string> {\r\n const metadata = await this.metadata();\r\n const format = this.options.format || metadata.format || \"png\";\r\n const mimeType = `image/${format === \"jpg\" ? \"jpeg\" : format}`;\r\n const base64 = await this.toBase64();\r\n\r\n return `data:${mimeType};base64,${base64}`;\r\n }\r\n\r\n /**\r\n * Sharpen image\r\n */\r\n public sharpen(options?: sharp.SharpenOptions): this {\r\n return this.addOperation({ type: \"sharpen\", options });\r\n }\r\n\r\n /**\r\n * Negate/invert image colors\r\n */\r\n public negate(options?: sharp.NegateOptions): this {\r\n return this.addOperation({ type: \"negate\", options });\r\n }\r\n\r\n /**\r\n * Tint image with a color\r\n */\r\n public tint(color: sharp.Color): this {\r\n return this.addOperation({ type: \"tint\", color });\r\n }\r\n\r\n /**\r\n * Trim edges from the image\r\n */\r\n public trim(options?: sharp.TrimOptions): this {\r\n return this.addOperation({ type: \"trim\", options });\r\n }\r\n\r\n /**\r\n * Convert to buffer\r\n */\r\n public async toBuffer(): Promise<Buffer> {\r\n const image = await this.executePipeline();\r\n\r\n return image.toBuffer();\r\n }\r\n\r\n /**\r\n * Clone the image for separate transformations\r\n */\r\n public clone(): Image {\r\n const clonedImage = new Image(this.image.clone());\r\n clonedImage.options = { ...this.options };\r\n clonedImage.operations = [...this.operations];\r\n clonedImage.cachedMetadata = this.cachedMetadata ? { ...this.cachedMetadata } : null;\r\n\r\n return clonedImage;\r\n }\r\n\r\n /**\r\n * Get the current stored options\r\n */\r\n public getOptions(): Readonly<InternalOptions> {\r\n return { ...this.options };\r\n }\r\n\r\n /**\r\n * Get the pending operations count\r\n */\r\n public getPendingOperationsCount(): number {\r\n return this.operations.length;\r\n }\r\n\r\n /**\r\n * Reset all stored options\r\n */\r\n public resetOptions(): this {\r\n this.options = {};\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Clear all pending operations\r\n */\r\n public clearOperations(): this {\r\n this.operations = [];\r\n this.pipelineExecuted = false;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Reset the image to its initial state (clear operations and options)\r\n */\r\n public reset(): this {\r\n this.operations = [];\r\n this.options = {};\r\n this.pipelineExecuted = false;\r\n this.cachedMetadata = null;\r\n\r\n return this;\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"image.mjs","names":[],"sources":["../../../../../../../core/src/image/image.ts"],"sourcesContent":["import { http } from \"@mongez/http\";\r\nimport { createRequire } from \"node:module\";\r\nimport type sharp from \"sharp\";\r\nimport type { FormatEnum } from \"sharp\";\r\n\r\n// ============================================================\r\n// Lazily Resolved Sharp Module\r\n// ============================================================\r\n\r\n/**\r\n * Installation instructions for sharp\r\n */\r\nconst SHARP_INSTALL_INSTRUCTIONS = `\r\nImage processing requires the sharp package.\r\nInstall it with:\r\n\r\n warlock add image\r\n\r\nOr manually:\r\n\r\n npm install sharp\r\n pnpm add sharp\r\n yarn add sharp\r\n`.trim();\r\n\r\n/**\r\n * Cached sharp function, populated by the first `resolveSharp()` call\r\n */\r\nlet sharpFn: typeof sharp | null = null;\r\n\r\n/**\r\n * Whether the resolution attempt already ran (success or failure)\r\n */\r\nlet sharpResolved = false;\r\n\r\n/**\r\n * Why the resolution failed, when it failed for a reason other than absence.\r\n *\r\n * Cached and re-thrown on every later call: the resolution attempt runs exactly\r\n * once, so without this the second construction would fall through to the\r\n * \"not installed\" branch and report a different, wrong cause.\r\n */\r\nlet sharpLoadError: Error | null = null;\r\n\r\n/**\r\n * `new Error(message, { cause })` is ES2022, and this package still compiles\r\n * against the ES2020 lib (see #16), where `Error` is typed as taking a message\r\n * only. Node has supported the option since v16, so this is the type layer\r\n * catching up with the runtime, not a change in what the code does.\r\n */\r\ntype ErrorWithCauseConstructor = new (message: string, options: { cause: unknown }) => Error;\r\n\r\nconst ErrorWithCause = Error as ErrorWithCauseConstructor;\r\n\r\n/**\r\n * Whether `error` says sharp itself is absent, as opposed to present but broken.\r\n *\r\n * Both halves are load-bearing. `MODULE_NOT_FOUND` alone is not enough: a\r\n * dependency missing *inside* sharp raises the very same code (`Cannot find\r\n * module 'color'`), and treating that as absence would tell the operator to\r\n * install a package they already have. So the message has to name the specifier\r\n * `'sharp'` exactly — quoted, which is also what keeps `'sharp-cli'` and friends\r\n * from matching.\r\n */\r\nfunction isSharpMissing(error: unknown): boolean {\r\n if (!(error instanceof Error)) return false;\r\n\r\n return (\r\n (error as NodeJS.ErrnoException).code === \"MODULE_NOT_FOUND\" &&\r\n error.message.includes(\"Cannot find module 'sharp'\")\r\n );\r\n}\r\n\r\n/**\r\n * Resolve sharp synchronously, on first use.\r\n *\r\n * Resolution is deliberately *lazy* and *synchronous*:\r\n *\r\n * - Lazy, because a top-level require would drag sharp's native binary into\r\n * every `import \"@warlock.js/core\"`, including apps that never touch images.\r\n * - Synchronous, because the `Image` constructor is synchronous. An async\r\n * import kicked off at module load leaves a window in which the module is\r\n * neither loaded nor known to be missing, and a constructor running inside\r\n * that window has no correct answer to give.\r\n *\r\n * `createRequire` gives an ESM-safe `require`, and the outcome — module, absence\r\n * or load failure — is cached, so the resolution runs exactly once per process.\r\n * The failure *reason* is cached too, not just the fact of it: every call after\r\n * a failed load has to report the same cause as the first one.\r\n *\r\n * @throws when sharp is not installed, or is installed but cannot be loaded\r\n */\r\nfunction resolveSharp(): typeof sharp {\r\n if (!sharpResolved) {\r\n sharpResolved = true;\r\n\r\n try {\r\n const require = createRequire(import.meta.url);\r\n const module = require(\"sharp\");\r\n sharpFn = (module.default ?? module) as typeof sharp;\r\n } catch (error) {\r\n sharpFn = null;\r\n\r\n if (!isSharpMissing(error)) {\r\n // sharp is there, it just would not load — almost always a binary built\r\n // for another platform. Its own error names the runtime and the fix, so\r\n // it is inlined *and* chained: a terminal that never prints `cause`\r\n // must still show the text that actually helps.\r\n sharpLoadError = new ErrorWithCause(`Failed to load \"sharp\": ${(error as Error).message}`, {\r\n cause: error,\r\n });\r\n }\r\n }\r\n }\r\n\r\n if (sharpLoadError) {\r\n throw sharpLoadError;\r\n }\r\n\r\n if (!sharpFn) {\r\n throw new Error(`sharp is not installed.\\n\\n${SHARP_INSTALL_INSTRUCTIONS}`);\r\n }\r\n\r\n return sharpFn;\r\n}\r\n\r\n// ============================================================\r\n// Types\r\n// ============================================================\r\n\r\nexport type ImageFormat = keyof FormatEnum;\r\n\r\nexport type ImageInput = string | Buffer | Uint8Array | ArrayBuffer;\r\n\r\n/**\r\n * Watermark configuration for deferred execution\r\n */\r\nexport type WatermarkConfig = {\r\n image: ImageInput | Image;\r\n options: sharp.OverlayOptions;\r\n};\r\n\r\n/**\r\n * Operation descriptor for deferred pipeline execution.\r\n * All operations are stored and executed at save/toBuffer time.\r\n */\r\ntype ImageOperation =\r\n | { type: \"resize\"; options: sharp.ResizeOptions }\r\n | { type: \"crop\"; options: sharp.Region }\r\n | { type: \"rotate\"; angle: number }\r\n | { type: \"flip\" }\r\n | { type: \"flop\" }\r\n | { type: \"blur\"; sigma: number }\r\n | { type: \"sharpen\"; options?: sharp.SharpenOptions }\r\n | { type: \"blackAndWhite\" }\r\n | { type: \"opacity\"; value: number }\r\n | { type: \"negate\"; options?: sharp.NegateOptions }\r\n | { type: \"tint\"; color: sharp.Color }\r\n | { type: \"trim\"; options?: sharp.TrimOptions }\r\n | { type: \"watermark\"; config: WatermarkConfig }\r\n | { type: \"watermarks\"; configs: WatermarkConfig[] };\r\n\r\n/**\r\n * Transformation options that can be applied in batch via `apply()` method.\r\n *\r\n * **Execution Order (when using apply()):**\r\n * 1. resize - Resize first to work with correct dimensions\r\n * 2. crop - Crop after resize to extract the desired region\r\n * 3. rotate - Rotation after sizing\r\n * 4. flip/flop - Mirror operations\r\n * 5. blackAndWhite/grayscale - Color space conversion\r\n * 6. blur - Blur effect\r\n * 7. sharpen - Sharpen effect\r\n * 8. tint - Color overlay\r\n * 9. negate - Invert colors\r\n * 10. opacity - Transparency (applied via composite)\r\n * 11. format/quality - Applied on save/export\r\n */\r\nexport type ImageTransformOptions = {\r\n /**\r\n * Output quality (1-100), applied based on final format\r\n */\r\n quality?: number;\r\n /**\r\n * Output format (jpeg, png, webp, avif, etc.)\r\n */\r\n format?: ImageFormat;\r\n /**\r\n * Resize options\r\n */\r\n resize?: sharp.ResizeOptions;\r\n /**\r\n * Crop/extract region\r\n */\r\n crop?: sharp.Region;\r\n /**\r\n * Rotation angle in degrees\r\n */\r\n rotate?: number;\r\n /**\r\n * Flip vertically (top to bottom)\r\n */\r\n flip?: boolean;\r\n /**\r\n * Flop horizontally (left to right)\r\n */\r\n flop?: boolean;\r\n /**\r\n * Convert to black and white\r\n */\r\n blackAndWhite?: boolean;\r\n /**\r\n * Alias for blackAndWhite\r\n */\r\n grayscale?: boolean;\r\n /**\r\n * Blur sigma (must be >= 0.3)\r\n */\r\n blur?: number;\r\n /**\r\n * Sharpen options\r\n */\r\n sharpen?: sharp.SharpenOptions | boolean;\r\n /**\r\n * Tint color\r\n */\r\n tint?: sharp.Color;\r\n /**\r\n * Negate/invert colors\r\n */\r\n negate?: sharp.NegateOptions | boolean;\r\n /**\r\n * Opacity (0-100)\r\n */\r\n opacity?: number;\r\n /**\r\n * Trim options\r\n */\r\n trim?: sharp.TrimOptions | boolean;\r\n /**\r\n * Single watermark\r\n */\r\n watermark?: WatermarkConfig;\r\n /**\r\n * Multiple watermarks\r\n */\r\n watermarks?: WatermarkConfig[];\r\n};\r\n\r\n/**\r\n * Internal options stored for deferred application\r\n */\r\ntype InternalOptions = {\r\n quality?: number;\r\n format?: ImageFormat;\r\n};\r\n\r\n/**\r\n * Image manipulation class with deferred pipeline execution.\r\n *\r\n * **Important:** This class requires the `sharp` package to be installed.\r\n * Install it with: `warlock add image` or `npm install sharp`\r\n *\r\n * Sharp is resolved synchronously on the first construction that needs it, so\r\n * the constructor and all chainable methods remain synchronous, and a missing\r\n * sharp throws at construction rather than at some later await.\r\n *\r\n * All operations are synchronous and stored as descriptors.\r\n * The pipeline is executed only when calling output methods:\r\n * - `save()` - Save to file\r\n * - `toBuffer()` - Get as buffer\r\n * - `toBase64()` - Get as base64 string\r\n * - `toDataUrl()` - Get as data URL\r\n *\r\n * @example\r\n * ```typescript\r\n * // All chaining is synchronous - single await at the end\r\n * await new Image(\"photo.jpg\")\r\n * .resize({ width: 800 })\r\n * .watermark(\"logo.png\", { gravity: \"southeast\" })\r\n * .quality(85)\r\n * .save(\"output.jpg\");\r\n * ```\r\n */\r\nexport class Image {\r\n /**\r\n * Image options that will be applied on save/export\r\n */\r\n protected options: InternalOptions = {};\r\n\r\n /**\r\n * Deferred operations pipeline\r\n */\r\n protected operations: ImageOperation[] = [];\r\n\r\n /**\r\n * Cached metadata to avoid repeated async calls\r\n */\r\n protected cachedMetadata: sharp.Metadata | null = null;\r\n\r\n /**\r\n * Whether the pipeline has been executed\r\n */\r\n protected pipelineExecuted = false;\r\n\r\n /**\r\n * Sharp image object\r\n */\r\n public readonly image: sharp.Sharp;\r\n\r\n /**\r\n * Formats that support quality option\r\n */\r\n protected static readonly QUALITY_FORMATS = [\"jpeg\", \"jpg\", \"webp\", \"avif\", \"tiff\", \"heif\"];\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(image: ImageInput | sharp.Sharp) {\r\n // An existing sharp instance carries its own module — short-circuit before\r\n // resolving sharp, so cloning never pays for (or fails on) a module load.\r\n if (image instanceof Object && \"clone\" in image && typeof image.clone === \"function\") {\r\n this.image = image as sharp.Sharp;\r\n } else {\r\n this.image = resolveSharp()(image as ImageInput);\r\n }\r\n }\r\n\r\n /**\r\n * Create image instance from file path\r\n */\r\n public static fromFile(path: string): Image {\r\n return new Image(path);\r\n }\r\n\r\n /**\r\n * Create image instance from buffer\r\n */\r\n public static fromBuffer(buffer: Buffer): Image {\r\n return new Image(buffer);\r\n }\r\n\r\n /**\r\n * Create image instance from url\r\n */\r\n public static async fromUrl(url: string): Promise<Image> {\r\n const { data, error } = await http.get<ArrayBuffer>(url, {\r\n responseType: \"arrayBuffer\",\r\n });\r\n\r\n if (error || !data) {\r\n throw new Error(\r\n `Failed to load image from URL \"${url}\": ${error?.message ?? \"Empty response received\"}`,\r\n );\r\n }\r\n\r\n return new Image(Buffer.from(data));\r\n }\r\n\r\n /**\r\n * Add an operation to the deferred pipeline\r\n */\r\n protected addOperation(operation: ImageOperation): this {\r\n this.operations.push(operation);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Apply multiple transformations at once with a predefined execution order.\r\n *\r\n * This method ensures transformations are applied in a logical order:\r\n * resize → crop → rotate → flip/flop → colorspace → effects → opacity → format\r\n *\r\n * For custom ordering, use individual chained methods instead.\r\n */\r\n public apply(options: ImageTransformOptions): this {\r\n // 1. Resize first to work with correct dimensions\r\n if (options.resize) {\r\n this.resize(options.resize);\r\n }\r\n\r\n // 2. Crop after resize\r\n if (options.crop) {\r\n this.crop(options.crop);\r\n }\r\n\r\n // 3. Rotation\r\n if (options.rotate !== undefined) {\r\n this.rotate(options.rotate);\r\n }\r\n\r\n // 4. Mirror operations\r\n if (options.flip) {\r\n this.flip();\r\n }\r\n\r\n if (options.flop) {\r\n this.flop();\r\n }\r\n\r\n // 5. Color space conversion\r\n if (options.blackAndWhite || options.grayscale) {\r\n this.blackAndWhite();\r\n }\r\n\r\n // 6. Blur effect\r\n if (options.blur !== undefined) {\r\n this.blur(options.blur);\r\n }\r\n\r\n // 7. Sharpen effect\r\n if (options.sharpen) {\r\n const sharpenOptions = typeof options.sharpen === \"boolean\" ? undefined : options.sharpen;\r\n this.sharpen(sharpenOptions);\r\n }\r\n\r\n // 8. Tint color\r\n if (options.tint) {\r\n this.tint(options.tint);\r\n }\r\n\r\n // 9. Negate/invert\r\n if (options.negate) {\r\n const negateOptions = typeof options.negate === \"boolean\" ? undefined : options.negate;\r\n this.negate(negateOptions);\r\n }\r\n\r\n // 10. Trim edges\r\n if (options.trim) {\r\n const trimOptions = typeof options.trim === \"boolean\" ? undefined : options.trim;\r\n this.trim(trimOptions);\r\n }\r\n\r\n // 11. Watermarks\r\n if (options.watermark) {\r\n this.watermark(options.watermark.image, options.watermark.options);\r\n }\r\n\r\n if (options.watermarks) {\r\n this.watermarks(options.watermarks);\r\n }\r\n\r\n // 12. Opacity (applied via composite)\r\n if (options.opacity !== undefined) {\r\n this.opacity(options.opacity);\r\n }\r\n\r\n // 13. Store format/quality for deferred application\r\n if (options.format) {\r\n this.format(options.format);\r\n }\r\n\r\n if (options.quality !== undefined) {\r\n this.quality(options.quality);\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Set image opacity (0-100)\r\n */\r\n public opacity(value: number): this {\r\n if (value < 0 || value > 100) {\r\n throw new Error(\"Opacity must be between 0 and 100\");\r\n }\r\n\r\n return this.addOperation({ type: \"opacity\", value });\r\n }\r\n\r\n /**\r\n * Convert image to black and white\r\n */\r\n public blackAndWhite(): this {\r\n return this.addOperation({ type: \"blackAndWhite\" });\r\n }\r\n\r\n /**\r\n * Alias for blackAndWhite\r\n */\r\n public grayscale(): this {\r\n return this.blackAndWhite();\r\n }\r\n\r\n /**\r\n * Get image dimensions (cached after first call)\r\n */\r\n public async dimensions(): Promise<{\r\n width: number | undefined;\r\n height: number | undefined;\r\n }> {\r\n const metadata = await this.metadata();\r\n\r\n return { width: metadata.width, height: metadata.height };\r\n }\r\n\r\n /**\r\n * Get image metadata (cached after first call)\r\n *\r\n * The metadata is cached to avoid repeated async operations.\r\n * Use `refreshMetadata()` to force a fresh fetch.\r\n */\r\n public async metadata(): Promise<sharp.Metadata> {\r\n if (!this.cachedMetadata) {\r\n this.cachedMetadata = await this.image.metadata();\r\n }\r\n\r\n return this.cachedMetadata;\r\n }\r\n\r\n /**\r\n * Force refresh of cached metadata\r\n *\r\n * Call this after transformations if you need updated metadata.\r\n */\r\n public async refreshMetadata(): Promise<sharp.Metadata> {\r\n this.cachedMetadata = await this.image.metadata();\r\n\r\n return this.cachedMetadata;\r\n }\r\n\r\n /**\r\n * Clear cached metadata\r\n */\r\n public clearMetadataCache(): this {\r\n this.cachedMetadata = null;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Resize image\r\n */\r\n public resize(options: sharp.ResizeOptions): this {\r\n if (typeof options.width !== \"undefined\" && !options.width) {\r\n delete options.width;\r\n }\r\n\r\n if (typeof options.height !== \"undefined\" && !options.height) {\r\n delete options.height;\r\n }\r\n\r\n return this.addOperation({ type: \"resize\", options });\r\n }\r\n\r\n /**\r\n * Crop/extract a region from the image\r\n */\r\n public crop(options: sharp.Region): this {\r\n return this.addOperation({ type: \"crop\", options });\r\n }\r\n\r\n /**\r\n * Set image quality (1-100)\r\n * Quality is stored and applied when saving/exporting\r\n * based on the final format.\r\n */\r\n public quality(quality: number): this {\r\n if (quality < 1 || quality > 100) {\r\n throw new Error(\"Quality must be between 1 and 100\");\r\n }\r\n\r\n this.options.quality = quality;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Execute the deferred pipeline - apply all stored operations\r\n */\r\n protected async executePipeline(): Promise<sharp.Sharp> {\r\n if (this.pipelineExecuted) {\r\n return this.image;\r\n }\r\n\r\n for (const operation of this.operations) {\r\n await this.executeOperation(this.image, operation);\r\n }\r\n\r\n await this.applyFormatAndQuality(this.image);\r\n\r\n this.pipelineExecuted = true;\r\n\r\n return this.image;\r\n }\r\n\r\n /**\r\n * Execute a single operation\r\n */\r\n protected async executeOperation(image: sharp.Sharp, operation: ImageOperation): Promise<void> {\r\n switch (operation.type) {\r\n case \"resize\":\r\n image.resize(operation.options);\r\n break;\r\n\r\n case \"crop\":\r\n image.extract(operation.options);\r\n break;\r\n\r\n case \"rotate\":\r\n image.rotate(operation.angle);\r\n break;\r\n\r\n case \"flip\":\r\n image.flip();\r\n break;\r\n\r\n case \"flop\":\r\n image.flop();\r\n break;\r\n\r\n case \"blur\":\r\n image.blur(operation.sigma);\r\n break;\r\n\r\n case \"sharpen\":\r\n image.sharpen(operation.options);\r\n break;\r\n\r\n case \"blackAndWhite\":\r\n image.toColourspace(\"b-w\");\r\n break;\r\n\r\n case \"opacity\": {\r\n const alpha = Math.round((operation.value / 100) * 255);\r\n const alphaPixel = Buffer.from([255, 255, 255, alpha]);\r\n image.composite([\r\n {\r\n blend: \"dest-in\",\r\n input: alphaPixel,\r\n },\r\n ]);\r\n break;\r\n }\r\n\r\n case \"negate\":\r\n image.negate(operation.options);\r\n break;\r\n\r\n case \"tint\":\r\n image.tint(operation.color);\r\n break;\r\n\r\n case \"trim\":\r\n image.trim(operation.options);\r\n break;\r\n\r\n case \"watermark\": {\r\n const buffer = await this.resolveImageBuffer(operation.config.image);\r\n image.composite([\r\n {\r\n input: buffer,\r\n ...operation.config.options,\r\n },\r\n ]);\r\n break;\r\n }\r\n\r\n case \"watermarks\": {\r\n const buffers = await Promise.all(\r\n operation.configs.map((config) => this.resolveImageBuffer(config.image)),\r\n );\r\n image.composite(\r\n operation.configs.map((config, index) => ({\r\n input: buffers[index],\r\n ...config.options,\r\n })),\r\n );\r\n break;\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Resolve an image input to a buffer\r\n */\r\n protected async resolveImageBuffer(input: ImageInput | Image): Promise<Buffer> {\r\n if (input instanceof Image) {\r\n // For Image instances, get buffer without applying options (raw buffer)\r\n return input.image.toBuffer();\r\n }\r\n\r\n // For other inputs (path, buffer, etc.), create temp sharp instance\r\n const tempImage = resolveSharp()(input);\r\n\r\n return tempImage.toBuffer();\r\n }\r\n\r\n /**\r\n * Apply format and quality options.\r\n * If no format is explicitly set, preserves the original format and applies\r\n * quality appropriately based on the format type.\r\n */\r\n protected async applyFormatAndQuality(image: sharp.Sharp): Promise<void> {\r\n const { quality, format } = this.options;\r\n\r\n if (format) {\r\n // Explicit format specified\r\n const formatOptions = quality ? { quality } : undefined;\r\n image.toFormat(format, formatOptions);\r\n return;\r\n }\r\n\r\n if (quality === undefined) {\r\n // No quality or format set, nothing to apply\r\n return;\r\n }\r\n\r\n // Quality is set but no format specified - detect original format\r\n const metadata = await this.metadata();\r\n const originalFormat = metadata.format;\r\n\r\n if (!originalFormat) {\r\n // Cannot detect format, default to webp with quality\r\n image.webp({ quality });\r\n return;\r\n }\r\n\r\n // Apply quality based on original format\r\n if (Image.QUALITY_FORMATS.includes(originalFormat)) {\r\n // Format supports quality option\r\n image.toFormat(originalFormat as ImageFormat, { quality });\r\n } else if (originalFormat === \"png\") {\r\n // PNG uses compressionLevel (0-9) instead of quality\r\n // Map quality 1-100 to compressionLevel 9-0 (higher quality = lower compression)\r\n const compressionLevel = Math.round(9 - (quality / 100) * 9);\r\n image.png({ compressionLevel });\r\n } else if (originalFormat === \"gif\") {\r\n // GIF doesn't support quality, just preserve format\r\n image.gif();\r\n }\r\n // Unknown format: preserve as-is (no quality applied)\r\n }\r\n\r\n /**\r\n * Save to file\r\n */\r\n public async save(path: string): Promise<sharp.OutputInfo> {\r\n const image = await this.executePipeline();\r\n\r\n return image.toFile(path);\r\n }\r\n\r\n /**\r\n * Convert to webp and save to file\r\n */\r\n public async saveAsWebp(path: string): Promise<sharp.OutputInfo> {\r\n // Override format to webp\r\n this.options.format = \"webp\";\r\n const image = await this.executePipeline();\r\n\r\n return image.toFile(path);\r\n }\r\n\r\n /**\r\n * Change the file format\r\n */\r\n public format(format: ImageFormat): this {\r\n this.options.format = format;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Add watermark (deferred - executed at save time)\r\n */\r\n public watermark(image: ImageInput | Image, options: sharp.OverlayOptions = {}): this {\r\n return this.addOperation({\r\n type: \"watermark\",\r\n config: { image, options },\r\n });\r\n }\r\n\r\n /**\r\n * Add multiple watermarks (deferred - executed at save time)\r\n */\r\n public watermarks(configs: WatermarkConfig[]): this {\r\n return this.addOperation({\r\n type: \"watermarks\",\r\n configs,\r\n });\r\n }\r\n\r\n /**\r\n * Rotate image\r\n */\r\n public rotate(angle: number): this {\r\n return this.addOperation({ type: \"rotate\", angle });\r\n }\r\n\r\n /**\r\n * Flip image vertically (top to bottom)\r\n */\r\n public flip(): this {\r\n return this.addOperation({ type: \"flip\" });\r\n }\r\n\r\n /**\r\n * Flop image horizontally (left to right)\r\n */\r\n public flop(): this {\r\n return this.addOperation({ type: \"flop\" });\r\n }\r\n\r\n /**\r\n * Blur image\r\n */\r\n public blur(sigma: number): this {\r\n if (sigma < 0.3) {\r\n throw new Error(\"Blur sigma must be at least 0.3\");\r\n }\r\n\r\n return this.addOperation({ type: \"blur\", sigma });\r\n }\r\n\r\n /**\r\n * Convert to base64\r\n */\r\n public async toBase64(): Promise<string> {\r\n const image = await this.executePipeline();\r\n const buffer = await image.toBuffer();\r\n\r\n return buffer.toString(\"base64\");\r\n }\r\n\r\n /**\r\n * Convert to data URL (base64 with mime type prefix)\r\n */\r\n public async toDataUrl(): Promise<string> {\r\n const metadata = await this.metadata();\r\n const format = this.options.format || metadata.format || \"png\";\r\n const mimeType = `image/${format === \"jpg\" ? \"jpeg\" : format}`;\r\n const base64 = await this.toBase64();\r\n\r\n return `data:${mimeType};base64,${base64}`;\r\n }\r\n\r\n /**\r\n * Sharpen image\r\n */\r\n public sharpen(options?: sharp.SharpenOptions): this {\r\n return this.addOperation({ type: \"sharpen\", options });\r\n }\r\n\r\n /**\r\n * Negate/invert image colors\r\n */\r\n public negate(options?: sharp.NegateOptions): this {\r\n return this.addOperation({ type: \"negate\", options });\r\n }\r\n\r\n /**\r\n * Tint image with a color\r\n */\r\n public tint(color: sharp.Color): this {\r\n return this.addOperation({ type: \"tint\", color });\r\n }\r\n\r\n /**\r\n * Trim edges from the image\r\n */\r\n public trim(options?: sharp.TrimOptions): this {\r\n return this.addOperation({ type: \"trim\", options });\r\n }\r\n\r\n /**\r\n * Convert to buffer\r\n */\r\n public async toBuffer(): Promise<Buffer> {\r\n const image = await this.executePipeline();\r\n\r\n return image.toBuffer();\r\n }\r\n\r\n /**\r\n * Clone the image for separate transformations\r\n */\r\n public clone(): Image {\r\n const clonedImage = new Image(this.image.clone());\r\n clonedImage.options = { ...this.options };\r\n clonedImage.operations = [...this.operations];\r\n clonedImage.cachedMetadata = this.cachedMetadata ? { ...this.cachedMetadata } : null;\r\n\r\n return clonedImage;\r\n }\r\n\r\n /**\r\n * Get the current stored options\r\n */\r\n public getOptions(): Readonly<InternalOptions> {\r\n return { ...this.options };\r\n }\r\n\r\n /**\r\n * Get the pending operations count\r\n */\r\n public getPendingOperationsCount(): number {\r\n return this.operations.length;\r\n }\r\n\r\n /**\r\n * Reset all stored options\r\n */\r\n public resetOptions(): this {\r\n this.options = {};\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Clear all pending operations\r\n */\r\n public clearOperations(): this {\r\n this.operations = [];\r\n this.pipelineExecuted = false;\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Reset the image to its initial state (clear operations and options)\r\n */\r\n public reset(): this {\r\n this.operations = [];\r\n this.options = {};\r\n this.pipelineExecuted = false;\r\n this.cachedMetadata = null;\r\n\r\n return this;\r\n }\r\n}\r\n"],"mappings":"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"}
|