@gencow/core 0.1.27 → 0.1.29

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.
Files changed (130) hide show
  1. package/dist/auth-config.d.ts +92 -5
  2. package/dist/config.d.ts +107 -0
  3. package/dist/config.js +12 -0
  4. package/dist/context.d.ts +139 -0
  5. package/dist/context.js +3 -0
  6. package/dist/crud.d.ts +5 -5
  7. package/dist/crud.js +19 -35
  8. package/dist/document-types.d.ts +65 -0
  9. package/dist/document-types.js +15 -0
  10. package/dist/grounded-answer-types.d.ts +62 -0
  11. package/dist/grounded-answer-types.js +6 -0
  12. package/dist/http-action.d.ts +77 -0
  13. package/dist/http-action.js +41 -0
  14. package/dist/index.d.ts +30 -5
  15. package/dist/index.js +15 -2
  16. package/dist/platform-capacity-profile.d.ts +19 -0
  17. package/dist/platform-capacity-profile.js +94 -0
  18. package/dist/procedure.d.ts +58 -0
  19. package/dist/procedure.js +115 -0
  20. package/dist/rag-ingest-types.d.ts +39 -0
  21. package/dist/rag-ingest-types.js +1 -0
  22. package/dist/rag-operations-types.d.ts +81 -0
  23. package/dist/rag-operations-types.js +1 -0
  24. package/dist/rag-schema.d.ts +1466 -0
  25. package/dist/rag-schema.js +87 -0
  26. package/dist/reactive-mutation-types.d.ts +11 -0
  27. package/dist/reactive-mutation-types.js +1 -0
  28. package/dist/reactive-mutation.d.ts +51 -0
  29. package/dist/reactive-mutation.js +75 -0
  30. package/dist/reactive-query-types.d.ts +12 -0
  31. package/dist/reactive-query-types.js +1 -0
  32. package/dist/reactive-query.d.ts +14 -0
  33. package/dist/reactive-query.js +28 -0
  34. package/dist/reactive-realtime.d.ts +48 -0
  35. package/dist/reactive-realtime.js +236 -0
  36. package/dist/reactive.d.ts +29 -5
  37. package/dist/reactive.js +65 -0
  38. package/dist/rls-db.d.ts +9 -2
  39. package/dist/runtime-env-policy.d.ts +5 -0
  40. package/dist/runtime-env-policy.js +56 -0
  41. package/dist/search-types.d.ts +83 -0
  42. package/dist/search-types.js +1 -0
  43. package/dist/server.d.ts +1 -2
  44. package/dist/server.js +0 -1
  45. package/dist/storage-metering.d.ts +13 -0
  46. package/dist/storage-metering.js +18 -0
  47. package/dist/storage-shared.d.ts +36 -0
  48. package/dist/storage-shared.js +39 -0
  49. package/dist/storage.d.ts +5 -27
  50. package/dist/storage.js +30 -22
  51. package/dist/wake-app-result.d.ts +22 -0
  52. package/dist/wake-app-result.js +11 -0
  53. package/dist/workflow-types.d.ts +16 -2
  54. package/dist/workflow.d.ts +1 -1
  55. package/dist/workflow.js +136 -11
  56. package/dist/workflows-api.js +71 -3
  57. package/package.json +11 -7
  58. package/src/auth-config.ts +104 -3
  59. package/src/config.ts +119 -0
  60. package/src/context.ts +152 -0
  61. package/src/crud.ts +18 -35
  62. package/src/document-types.ts +102 -0
  63. package/src/grounded-answer-types.ts +78 -0
  64. package/src/http-action.ts +101 -0
  65. package/src/index.ts +142 -19
  66. package/src/platform-capacity-profile.ts +114 -0
  67. package/src/procedure.ts +283 -0
  68. package/src/rag-ingest-types.ts +52 -0
  69. package/src/rag-operations-types.ts +90 -0
  70. package/src/rag-schema.ts +94 -0
  71. package/src/reactive-mutation-types.ts +13 -0
  72. package/src/reactive-mutation.ts +115 -0
  73. package/src/reactive-query-types.ts +14 -0
  74. package/src/reactive-query.ts +48 -0
  75. package/src/reactive-realtime.ts +267 -0
  76. package/src/rls-db.ts +9 -4
  77. package/src/runtime-env-policy.ts +66 -0
  78. package/src/search-types.ts +91 -0
  79. package/src/server.ts +6 -2
  80. package/src/storage-metering.ts +35 -0
  81. package/src/storage-shared.ts +74 -0
  82. package/src/storage.ts +44 -53
  83. package/src/wake-app-result.ts +37 -0
  84. package/src/workflow-types.ts +16 -2
  85. package/src/workflow.ts +166 -12
  86. package/src/workflows-api.ts +82 -3
  87. package/src/__tests__/auth.test.ts +0 -118
  88. package/src/__tests__/crons.test.ts +0 -83
  89. package/src/__tests__/crud-codegen-integration.test.ts +0 -246
  90. package/src/__tests__/crud-owner-rls.test.ts +0 -387
  91. package/src/__tests__/crud.test.ts +0 -930
  92. package/src/__tests__/dist-exports.test.ts +0 -176
  93. package/src/__tests__/fixtures/basic/auth.ts +0 -32
  94. package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
  95. package/src/__tests__/fixtures/basic/index.ts +0 -6
  96. package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
  97. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
  98. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
  99. package/src/__tests__/fixtures/basic/schema.ts +0 -51
  100. package/src/__tests__/fixtures/basic/tasks.ts +0 -15
  101. package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
  102. package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
  103. package/src/__tests__/helpers/pglite-migrations.ts +0 -32
  104. package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
  105. package/src/__tests__/helpers/seed-like-fill.ts +0 -202
  106. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
  107. package/src/__tests__/httpaction.test.ts +0 -122
  108. package/src/__tests__/image-optimization.test.ts +0 -648
  109. package/src/__tests__/load.test.ts +0 -389
  110. package/src/__tests__/network-sim.test.ts +0 -319
  111. package/src/__tests__/reactive.test.ts +0 -479
  112. package/src/__tests__/retry.test.ts +0 -113
  113. package/src/__tests__/rls-crud-basic.test.ts +0 -317
  114. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
  115. package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
  116. package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
  117. package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
  118. package/src/__tests__/rls-session-and-policies.test.ts +0 -228
  119. package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
  120. package/src/__tests__/scheduler-durable.test.ts +0 -173
  121. package/src/__tests__/scheduler-exec.test.ts +0 -328
  122. package/src/__tests__/scheduler.test.ts +0 -187
  123. package/src/__tests__/storage.test.ts +0 -334
  124. package/src/__tests__/tsconfig.json +0 -8
  125. package/src/__tests__/validator.test.ts +0 -323
  126. package/src/__tests__/workflow.test.ts +0 -606
  127. package/src/__tests__/ws-integration.test.ts +0 -309
  128. package/src/__tests__/ws-scale.test.ts +0 -241
  129. package/src/auth.ts +0 -155
  130. package/src/reactive.ts +0 -580
@@ -0,0 +1,66 @@
1
+ const RESERVED_TENANT_RUNTIME_ENV_KEYS = new Set([
2
+ "PORT",
3
+ "DATABASE_URL",
4
+ "GENCOW_DB_URL",
5
+ "BETTER_AUTH_SECRET",
6
+ "BETTER_AUTH_URL",
7
+ "IS_PLATFORM",
8
+ "GENCOW_PLATFORM_CONFIG_FILE",
9
+ "GENCOW_PLATFORM_URL",
10
+ "GENCOW_PLATFORM_DB",
11
+ "PLATFORM_OPENAI_KEY",
12
+ "PLATFORM_GOOGLE_KEY",
13
+ "PLATFORM_INTERNAL_SECRET",
14
+ "INVITE_ONLY",
15
+ "RUNNER_TYPE",
16
+ "PGBOUNCER_PORT",
17
+ "GENCOW_FUNCTIONS",
18
+ "GENCOW_STORAGE",
19
+ "GENCOW_MIGRATIONS",
20
+ "GENCOW_APP_NAME",
21
+ "GENCOW_APP_DATA_DIR",
22
+ "GENCOW_INTERNAL_TOKEN",
23
+ "GENCOW_CRON_TOKEN",
24
+ "GENCOW_AI_PROXY_URL",
25
+ "GENCOW_AI_PROXY_URL_ALT",
26
+ "GENCOW_AI_PROXY_TOKEN",
27
+ "GENCOW_METERING_URL",
28
+ "GENCOW_METERING_URL_ALT",
29
+ "GENCOW_START_REASON",
30
+ "GENCOW_RESTART_ID",
31
+ "GENCOW_SHUTDOWN_MARKER_PATH",
32
+ "GENCOW_SKIP_MIGRATION",
33
+ "GENCOW_DB_MAX_CONNECTIONS",
34
+ "GENCOW_MEMORY_MB",
35
+ "BUN_JSC_forceRAMSize",
36
+ "MIMALLOC_PURGE_DELAY",
37
+ "NODE_PATH",
38
+ ]);
39
+
40
+ const RESERVED_TENANT_RUNTIME_ENV_PREFIXES = ["__GENCOW_", "GENCOW_DOCUMENT_", "GENCOW_TEMPLATE_", "GENCOW_WARM_"];
41
+
42
+ export function isReservedTenantRuntimeEnvKey(key: string): boolean {
43
+ const normalized = key.trim();
44
+ return (
45
+ RESERVED_TENANT_RUNTIME_ENV_KEYS.has(normalized) ||
46
+ RESERVED_TENANT_RUNTIME_ENV_PREFIXES.some((prefix) => normalized.startsWith(prefix))
47
+ );
48
+ }
49
+
50
+ export function filterTenantRuntimeEnvVars(vars: Record<string, string>): {
51
+ allowed: Record<string, string>;
52
+ rejectedKeys: string[];
53
+ } {
54
+ const allowed: Record<string, string> = {};
55
+ const rejectedKeys: string[] = [];
56
+
57
+ for (const [key, value] of Object.entries(vars)) {
58
+ if (isReservedTenantRuntimeEnvKey(key)) {
59
+ rejectedKeys.push(key);
60
+ continue;
61
+ }
62
+ allowed[key] = value;
63
+ }
64
+
65
+ return { allowed, rejectedKeys };
66
+ }
@@ -0,0 +1,91 @@
1
+ export type SearchPrimitive = string | number | boolean;
2
+
3
+ export type SearchScope = {
4
+ corpus: string;
5
+ visibility: "private" | "shared" | "public";
6
+ ownerUserId?: string;
7
+ };
8
+
9
+ export type SearchFilter = {
10
+ eq?: Record<string, SearchPrimitive>;
11
+ in?: Record<string, SearchPrimitive[]>;
12
+ range?: Record<string, { gte?: string | number; lte?: string | number }>;
13
+ };
14
+
15
+ export type SearchOptions = {
16
+ fields: [string, ...string[]];
17
+ limit?: number;
18
+ offset?: number;
19
+ filters?: SearchFilter;
20
+ scope: SearchScope;
21
+ };
22
+
23
+ export type VectorSearchTuning = {
24
+ minScore?: number;
25
+ };
26
+
27
+ export type VectorSearchOptions = Omit<SearchOptions, "fields"> & {
28
+ vector: number[];
29
+ vectorField?: string;
30
+ tuning?: VectorSearchTuning;
31
+ };
32
+
33
+ export type HybridSearchFusionTuning = {
34
+ mode?: "rrf";
35
+ rrfK?: number;
36
+ keywordWeight?: number;
37
+ vectorWeight?: number;
38
+ };
39
+
40
+ export type HybridSearchTuning = {
41
+ keywordCandidateLimit?: number;
42
+ vectorCandidateLimit?: number;
43
+ minFusedScore?: number;
44
+ fusion?: HybridSearchFusionTuning;
45
+ };
46
+
47
+ export type HybridSearchOptions = SearchOptions & {
48
+ vector: number[];
49
+ vectorField?: string;
50
+ fusion?: "rrf"; // legacy top-level compatibility
51
+ keywordCandidateLimit?: number; // legacy top-level compatibility
52
+ vectorCandidateLimit?: number; // legacy top-level compatibility
53
+ tuning?: HybridSearchTuning;
54
+ };
55
+
56
+ export type SearchHit = {
57
+ id: string | number;
58
+ row: Record<string, unknown>;
59
+ score: number;
60
+ scores?: {
61
+ keyword?: number;
62
+ vector?: number;
63
+ fused?: number;
64
+ };
65
+ matchedBy: Array<"keyword" | "vector">;
66
+ };
67
+
68
+ export type SearchResponse = {
69
+ items: SearchHit[];
70
+ meta: {
71
+ engine: "tsvector" | "pgroonga";
72
+ tier: "free" | "pro" | "scale";
73
+ limit: number;
74
+ offset: number;
75
+ nextOffset?: number;
76
+ fusion?: "rrf";
77
+ mode?: "keyword" | "vector" | "hybrid";
78
+ };
79
+ };
80
+
81
+ export type SearchTierConfig = {
82
+ plan: "free" | "pro" | "scale";
83
+ engine: "tsvector" | "pgroonga";
84
+ hybridSearch: boolean;
85
+ locale: "english" | "multilingual";
86
+ extensions: {
87
+ vector: boolean;
88
+ pgroonga: boolean;
89
+ };
90
+ degradedReason?: string;
91
+ };
package/src/server.ts CHANGED
@@ -6,6 +6,10 @@
6
6
  * bundled into user functions which run in Firecracker.
7
7
  */
8
8
  export { createStorage, storageRoutes } from "./storage.js";
9
- export type { StorageImageTierConfig } from "./storage.js";
9
+ export type {
10
+ StorageImageTierConfig,
11
+ StorageImageTransformMetric,
12
+ StorageMeteringOptions,
13
+ StoredFile,
14
+ } from "./storage.js";
10
15
  export { createScheduler, getSchedulerInfo } from "./scheduler.js";
11
- export { authMiddleware, authRoutes, getUsers } from "./auth.js";
@@ -0,0 +1,35 @@
1
+ import * as fs from "fs/promises";
2
+
3
+ export interface StorageImageTransformMetric {
4
+ transformCount: number;
5
+ sourceBytes: number;
6
+ outputBytes: number;
7
+ format: string;
8
+ autoWebp: boolean;
9
+ }
10
+
11
+ export interface StorageMeteringOptions {
12
+ onImageTransform?: (metric: StorageImageTransformMetric) => void | Promise<void>;
13
+ }
14
+
15
+ export async function recordStorageImageTransform(
16
+ options: StorageMeteringOptions | undefined,
17
+ cachePath: string,
18
+ metric: Omit<StorageImageTransformMetric, "transformCount" | "outputBytes">,
19
+ ): Promise<{ size: number }> {
20
+ const stats = await fs.stat(cachePath);
21
+ if (!options?.onImageTransform) return stats;
22
+
23
+ try {
24
+ await options.onImageTransform({
25
+ ...metric,
26
+ transformCount: 1,
27
+ outputBytes: stats.size,
28
+ });
29
+ } catch (error) {
30
+ const msg = error instanceof Error ? error.message : String(error);
31
+ console.warn(`[storage] image transform metering failed: ${msg.slice(0, 120)}`);
32
+ }
33
+
34
+ return stats;
35
+ }
@@ -0,0 +1,74 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+
4
+ /** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
5
+ export const MAX_FILE_SIZE = 50 * 1024 * 1024;
6
+
7
+ /** 기본 스토리지 쿼터: 1GB */
8
+ export const DEFAULT_STORAGE_QUOTA = 1024 * 1024 * 1024;
9
+
10
+ export interface StorageFile {
11
+ id: string;
12
+ name: string;
13
+ size: number;
14
+ type: string;
15
+ path: string;
16
+ }
17
+
18
+ export interface StoredFile {
19
+ id: string;
20
+ name: string;
21
+ size: number;
22
+ type: string;
23
+ buffer: Buffer;
24
+ }
25
+
26
+ export interface StorageOptions {
27
+ rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
28
+ storageQuota?: number;
29
+ }
30
+
31
+ export interface Storage {
32
+ store(file: File | Blob, filename?: string): Promise<string>;
33
+ storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
34
+ get(storageId: string): Promise<StoredFile | null>;
35
+ getUrl(storageId: string): string;
36
+ getMeta(storageId: string): Promise<StorageFile | null>;
37
+ delete(storageId: string): Promise<void>;
38
+ }
39
+
40
+ export function formatBytes(bytes: number): string {
41
+ if (bytes < 1024) return `${bytes}B`;
42
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
43
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
44
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
45
+ }
46
+
47
+ export async function loadStorageMeta(params: {
48
+ storageId: string;
49
+ dir: string;
50
+ metaStore: Map<string, StorageFile>;
51
+ }): Promise<StorageFile | null> {
52
+ const cached = params.metaStore.get(params.storageId);
53
+ if (cached) {
54
+ return cached;
55
+ }
56
+
57
+ const filePath = path.join(params.dir, params.storageId);
58
+ try {
59
+ const raw = await fs.readFile(`${filePath}.meta`, "utf-8");
60
+ const parsed = JSON.parse(raw) as { name?: unknown; type?: unknown; size?: unknown };
61
+ const stat = await fs.stat(filePath);
62
+ const meta: StorageFile = {
63
+ id: params.storageId,
64
+ name: typeof parsed.name === "string" ? parsed.name : params.storageId,
65
+ size: typeof parsed.size === "number" ? parsed.size : stat.size,
66
+ type: typeof parsed.type === "string" ? parsed.type : "application/octet-stream",
67
+ path: filePath,
68
+ };
69
+ params.metaStore.set(params.storageId, meta);
70
+ return meta;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
package/src/storage.ts CHANGED
@@ -2,51 +2,18 @@ import * as fs from "fs/promises";
2
2
  import * as fsSync from "fs";
3
3
  import * as path from "path";
4
4
  import * as crypto from "crypto";
5
-
6
- // ─── Constants ──────────────────────────────────────────
7
-
8
- /** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
9
- const MAX_FILE_SIZE = 50 * 1024 * 1024;
10
-
11
- /** 기본 스토리지 쿼터: 1GB */
12
- const DEFAULT_STORAGE_QUOTA = 1024 * 1024 * 1024;
13
-
14
- function formatBytes(bytes: number): string {
15
- if (bytes < 1024) return `${bytes}B`;
16
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
17
- if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
18
- return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
19
- }
20
-
21
- // ─── Types ──────────────────────────────────────────────
22
-
23
- interface StorageFile {
24
- id: string;
25
- name: string;
26
- size: number;
27
- type: string;
28
- path: string;
29
- }
30
-
31
- export interface StorageOptions {
32
- /** Raw SQL 실행 함수 — DB 자동 기록 + 쿼터 검증에 필요 */
33
- rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
34
- /** 앱별 스토리지 쿼터 (bytes). 0 = 무제한. 기본: 1GB */
35
- storageQuota?: number;
36
- }
37
-
38
- export interface Storage {
39
- /** Store a file and return a storageId — Convex의 ctx.storage.store() */
40
- store(file: File | Blob, filename?: string): Promise<string>;
41
- /** Store from raw buffer */
42
- storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
43
- /** Get a serving URL for the file — Convex의 ctx.storage.getUrl() */
44
- getUrl(storageId: string): string;
45
- /** Get file metadata */
46
- getMeta(storageId: string): Promise<StorageFile | null>;
47
- /** Delete a stored file — Convex의 ctx.storage.delete() */
48
- delete(storageId: string): Promise<void>;
49
- }
5
+ import {
6
+ DEFAULT_STORAGE_QUOTA,
7
+ MAX_FILE_SIZE,
8
+ formatBytes,
9
+ loadStorageMeta,
10
+ } from "./storage-shared.js";
11
+ import type { Storage, StorageFile, StorageOptions, StoredFile } from "./storage-shared.js";
12
+ import { recordStorageImageTransform } from "./storage-metering.js";
13
+ import type { StorageMeteringOptions } from "./storage-metering.js";
14
+
15
+ export type { Storage, StorageFile, StorageOptions, StoredFile } from "./storage-shared.js";
16
+ export type { StorageImageTransformMetric, StorageMeteringOptions } from "./storage-metering.js";
50
17
 
51
18
  // ─── Implementation ─────────────────────────────────────
52
19
 
@@ -96,6 +63,8 @@ async function checkStorageQuota(
96
63
  ): Promise<void> {
97
64
  if (quota <= 0) return; // 무제한
98
65
 
66
+ await ensureFilesTable(rawSql);
67
+
99
68
  const rows = await rawSql(`SELECT COALESCE(SUM(CAST(size AS BIGINT)), 0) AS total FROM _system_files`);
100
69
  const currentUsage = Number((rows[0] as Record<string, string>)?.total || "0");
101
70
  const projectedUsage = currentUsage + newFileSize;
@@ -147,7 +116,6 @@ export function createStorage(dir: string = "./uploads", options?: StorageOption
147
116
 
148
117
  // Ensure directory exists
149
118
  fs.mkdir(dir, { recursive: true }).catch(() => {});
150
-
151
119
  return {
152
120
  async store(file: File | Blob, filename?: string): Promise<string> {
153
121
  // 크기 제한 검증 (단일 파일)
@@ -248,8 +216,28 @@ export function createStorage(dir: string = "./uploads", options?: StorageOption
248
216
  return `/api/storage/${storageId}`;
249
217
  },
250
218
 
219
+ async get(storageId: string): Promise<StoredFile | null> {
220
+ const meta = await loadStorageMeta({ storageId, dir, metaStore });
221
+ if (!meta) {
222
+ return null;
223
+ }
224
+
225
+ const buffer = await fs.readFile(meta.path).catch(() => null);
226
+ if (!buffer) {
227
+ return null;
228
+ }
229
+
230
+ return {
231
+ id: meta.id,
232
+ name: meta.name,
233
+ size: meta.size,
234
+ type: meta.type,
235
+ buffer,
236
+ };
237
+ },
238
+
251
239
  async getMeta(storageId: string): Promise<StorageFile | null> {
252
- return metaStore.get(storageId) || null;
240
+ return loadStorageMeta({ storageId, dir, metaStore });
253
241
  },
254
242
 
255
243
  async delete(storageId: string): Promise<void> {
@@ -482,6 +470,7 @@ export function storageRoutes(
482
470
  rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>,
483
471
  storageDir?: string,
484
472
  tierConfig?: StorageImageTierConfig,
473
+ meteringOptions?: StorageMeteringOptions,
485
474
  ) {
486
475
  const baseTierConfig: Required<StorageImageTierConfig> = {
487
476
  autoWebp: tierConfig?.autoWebp ?? true,
@@ -753,15 +742,17 @@ export function storageRoutes(
753
742
 
754
743
  // 변환 실행 → 캐시에 저장
755
744
  await pipeline.toFile(cachePath);
745
+ const cacheStats = await recordStorageImageTransform(meteringOptions, cachePath, {
746
+ sourceBytes: meta.size,
747
+ format: outputFormat || meta.type.replace(/^image\//, ""),
748
+ autoWebp: isAutoWebp,
749
+ });
756
750
 
757
751
  // WebP/AVIF가 원본보다 큰 경우 → 캐시 삭제 + 원본 서빙
758
752
  // (Static Deploy와 동일 전략 — apps.ts L840-847)
759
- if (isAutoWebp) {
760
- const cacheStats = await fs.stat(cachePath);
761
- if (cacheStats.size >= meta.size) {
762
- await fs.unlink(cachePath).catch(() => {});
763
- return serveOriginal(c, meta);
764
- }
753
+ if (isAutoWebp && cacheStats.size >= meta.size) {
754
+ await fs.unlink(cachePath).catch(() => {});
755
+ return serveOriginal(c, meta);
765
756
  }
766
757
 
767
758
  return serveCachedFile(c, cachePath, transformParams, isAutoWebp, meta);
@@ -0,0 +1,37 @@
1
+ export type WakeAppSuccessStatus = "already_running" | "woke";
2
+ export type WakeAppDeferredStatus = "capacity_rejected" | "queue_timeout";
3
+
4
+ export type WakeAppSuccessResult = {
5
+ ok: true;
6
+ status: WakeAppSuccessStatus;
7
+ port: number;
8
+ };
9
+
10
+ export type WakeAppDeferredResult = {
11
+ ok: false;
12
+ status: WakeAppDeferredStatus;
13
+ retryAfterSec: number;
14
+ };
15
+
16
+ export type WakeAppBootFailedResult = {
17
+ ok: false;
18
+ status: "boot_failed";
19
+ error: string;
20
+ };
21
+
22
+ export type WakeAppResult = WakeAppSuccessResult | WakeAppDeferredResult | WakeAppBootFailedResult;
23
+
24
+ export const DEFAULT_WAKE_RETRY_AFTER_SEC = 30;
25
+
26
+ export function buildWakeAppSuccessResult(status: WakeAppSuccessStatus, port: number): WakeAppSuccessResult {
27
+ return { ok: true, status, port };
28
+ }
29
+
30
+ export function buildWakeAppBootFailedResult(error: unknown): WakeAppBootFailedResult {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ return { ok: false, status: "boot_failed", error: message };
33
+ }
34
+
35
+ export function isWakeAppDeferredResult(result: WakeAppResult): result is WakeAppDeferredResult {
36
+ return !result.ok && (result.status === "capacity_rejected" || result.status === "queue_timeout");
37
+ }
@@ -1,4 +1,5 @@
1
- import type { GencowCtx } from "./reactive.js";
1
+ import type { GencowCtx } from "./context.js";
2
+ import type { WorkflowDocumentServicesCtx } from "./document-types.js";
2
3
  import type { InferArgs } from "./v.js";
3
4
 
4
5
  export type WorkflowStatus = "pending" | "running" | "completed" | "failed";
@@ -28,6 +29,7 @@ export interface WorkflowSummary {
28
29
  derivedStatus: WorkflowDerivedStatus;
29
30
  currentStep: string | null;
30
31
  error: string | null;
32
+ errorCode: string | null;
31
33
  retryCount: number;
32
34
  maxRetries: number;
33
35
  maxDurationMs: number;
@@ -77,9 +79,13 @@ export interface WorkflowResumePayload {
77
79
  workflowId: string;
78
80
  }
79
81
 
80
- export interface WorkflowCtx extends GencowCtx {
82
+ export interface WorkflowCtx extends Omit<GencowCtx, "services"> {
81
83
  workflowId: string;
82
84
  workflowName: string;
85
+ runId?: string;
86
+ attempt?: number;
87
+ stepAttempt?: number;
88
+ services: GencowCtx["services"] & WorkflowDocumentServicesCtx;
83
89
  step<TResult>(name: string, run: () => Promise<TResult>): Promise<TResult>;
84
90
  sleep(duration: WorkflowDuration): Promise<void>;
85
91
  waitForEvent<TPayload = unknown>(name: string, timeout?: WorkflowDuration): Promise<TPayload>;
@@ -93,7 +99,11 @@ export type WorkflowHandler<TArgs, TReturn> = (wf: WorkflowCtx, args: TArgs) =>
93
99
  export interface WorkflowOptions<TSchema = any, TReturn = any> {
94
100
  args?: TSchema;
95
101
  public?: boolean;
102
+ version?: string;
96
103
  maxDuration?: WorkflowDuration;
104
+ maxActiveDuration?: WorkflowDuration;
105
+ lifecycleTimeout?: WorkflowDuration;
106
+ concurrency?: number;
97
107
  retries?: number;
98
108
  handler: WorkflowHandler<InferArgs<TSchema>, TReturn>;
99
109
  }
@@ -102,7 +112,11 @@ export interface WorkflowDef<TSchema = any, TReturn = any> {
102
112
  name: string;
103
113
  argsSchema?: TSchema;
104
114
  isPublic: boolean;
115
+ version?: string;
105
116
  maxDurationMs: number;
117
+ maxActiveDurationMs: number;
118
+ lifecycleTimeoutMs: number | null;
119
+ concurrency: number | null;
106
120
  maxRetries: number;
107
121
  handler: WorkflowHandler<InferArgs<TSchema>, TReturn>;
108
122
  }