@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.
- package/dist/auth-config.d.ts +92 -5
- package/dist/config.d.ts +107 -0
- package/dist/config.js +12 -0
- package/dist/context.d.ts +139 -0
- package/dist/context.js +3 -0
- package/dist/crud.d.ts +5 -5
- package/dist/crud.js +19 -35
- package/dist/document-types.d.ts +65 -0
- package/dist/document-types.js +15 -0
- package/dist/grounded-answer-types.d.ts +62 -0
- package/dist/grounded-answer-types.js +6 -0
- package/dist/http-action.d.ts +77 -0
- package/dist/http-action.js +41 -0
- package/dist/index.d.ts +30 -5
- package/dist/index.js +15 -2
- package/dist/platform-capacity-profile.d.ts +19 -0
- package/dist/platform-capacity-profile.js +94 -0
- package/dist/procedure.d.ts +58 -0
- package/dist/procedure.js +115 -0
- package/dist/rag-ingest-types.d.ts +39 -0
- package/dist/rag-ingest-types.js +1 -0
- package/dist/rag-operations-types.d.ts +81 -0
- package/dist/rag-operations-types.js +1 -0
- package/dist/rag-schema.d.ts +1466 -0
- package/dist/rag-schema.js +87 -0
- package/dist/reactive-mutation-types.d.ts +11 -0
- package/dist/reactive-mutation-types.js +1 -0
- package/dist/reactive-mutation.d.ts +51 -0
- package/dist/reactive-mutation.js +75 -0
- package/dist/reactive-query-types.d.ts +12 -0
- package/dist/reactive-query-types.js +1 -0
- package/dist/reactive-query.d.ts +14 -0
- package/dist/reactive-query.js +28 -0
- package/dist/reactive-realtime.d.ts +48 -0
- package/dist/reactive-realtime.js +236 -0
- package/dist/reactive.d.ts +29 -5
- package/dist/reactive.js +65 -0
- package/dist/rls-db.d.ts +9 -2
- package/dist/runtime-env-policy.d.ts +5 -0
- package/dist/runtime-env-policy.js +56 -0
- package/dist/search-types.d.ts +83 -0
- package/dist/search-types.js +1 -0
- package/dist/server.d.ts +1 -2
- package/dist/server.js +0 -1
- package/dist/storage-metering.d.ts +13 -0
- package/dist/storage-metering.js +18 -0
- package/dist/storage-shared.d.ts +36 -0
- package/dist/storage-shared.js +39 -0
- package/dist/storage.d.ts +5 -27
- package/dist/storage.js +30 -22
- package/dist/wake-app-result.d.ts +22 -0
- package/dist/wake-app-result.js +11 -0
- package/dist/workflow-types.d.ts +16 -2
- package/dist/workflow.d.ts +1 -1
- package/dist/workflow.js +136 -11
- package/dist/workflows-api.js +71 -3
- package/package.json +11 -7
- package/src/auth-config.ts +104 -3
- package/src/config.ts +119 -0
- package/src/context.ts +152 -0
- package/src/crud.ts +18 -35
- package/src/document-types.ts +102 -0
- package/src/grounded-answer-types.ts +78 -0
- package/src/http-action.ts +101 -0
- package/src/index.ts +142 -19
- package/src/platform-capacity-profile.ts +114 -0
- package/src/procedure.ts +283 -0
- package/src/rag-ingest-types.ts +52 -0
- package/src/rag-operations-types.ts +90 -0
- package/src/rag-schema.ts +94 -0
- package/src/reactive-mutation-types.ts +13 -0
- package/src/reactive-mutation.ts +115 -0
- package/src/reactive-query-types.ts +14 -0
- package/src/reactive-query.ts +48 -0
- package/src/reactive-realtime.ts +267 -0
- package/src/rls-db.ts +9 -4
- package/src/runtime-env-policy.ts +66 -0
- package/src/search-types.ts +91 -0
- package/src/server.ts +6 -2
- package/src/storage-metering.ts +35 -0
- package/src/storage-shared.ts +74 -0
- package/src/storage.ts +44 -53
- package/src/wake-app-result.ts +37 -0
- package/src/workflow-types.ts +16 -2
- package/src/workflow.ts +166 -12
- package/src/workflows-api.ts +82 -3
- package/src/__tests__/auth.test.ts +0 -118
- package/src/__tests__/crons.test.ts +0 -83
- package/src/__tests__/crud-codegen-integration.test.ts +0 -246
- package/src/__tests__/crud-owner-rls.test.ts +0 -387
- package/src/__tests__/crud.test.ts +0 -930
- package/src/__tests__/dist-exports.test.ts +0 -176
- package/src/__tests__/fixtures/basic/auth.ts +0 -32
- package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
- package/src/__tests__/fixtures/basic/index.ts +0 -6
- package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
- package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
- package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
- package/src/__tests__/fixtures/basic/schema.ts +0 -51
- package/src/__tests__/fixtures/basic/tasks.ts +0 -15
- package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
- package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
- package/src/__tests__/helpers/pglite-migrations.ts +0 -32
- package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
- package/src/__tests__/helpers/seed-like-fill.ts +0 -202
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
- package/src/__tests__/httpaction.test.ts +0 -122
- package/src/__tests__/image-optimization.test.ts +0 -648
- package/src/__tests__/load.test.ts +0 -389
- package/src/__tests__/network-sim.test.ts +0 -319
- package/src/__tests__/reactive.test.ts +0 -479
- package/src/__tests__/retry.test.ts +0 -113
- package/src/__tests__/rls-crud-basic.test.ts +0 -317
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
- package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
- package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
- package/src/__tests__/rls-session-and-policies.test.ts +0 -228
- package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
- package/src/__tests__/scheduler-durable.test.ts +0 -173
- package/src/__tests__/scheduler-exec.test.ts +0 -328
- package/src/__tests__/scheduler.test.ts +0 -187
- package/src/__tests__/storage.test.ts +0 -334
- package/src/__tests__/tsconfig.json +0 -8
- package/src/__tests__/validator.test.ts +0 -323
- package/src/__tests__/workflow.test.ts +0 -606
- package/src/__tests__/ws-integration.test.ts +0 -309
- package/src/__tests__/ws-scale.test.ts +0 -241
- package/src/auth.ts +0 -155
- package/src/reactive.ts +0 -580
package/dist/reactive.js
CHANGED
|
@@ -18,6 +18,40 @@ const subscribers = globalThis.__gencow_subscribers;
|
|
|
18
18
|
* to a specific query (e.g. the admin dashboard's raw WebSocket connection).
|
|
19
19
|
*/
|
|
20
20
|
const connectedClients = globalThis.__gencow_connectedClients;
|
|
21
|
+
const SUBSCRIPTION_KEY_SEPARATOR = "::";
|
|
22
|
+
function normalizeForStableJson(value) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (value === null)
|
|
26
|
+
return null;
|
|
27
|
+
if (value instanceof Date)
|
|
28
|
+
return value.toISOString();
|
|
29
|
+
if (Array.isArray(value))
|
|
30
|
+
return value.map((item) => normalizeForStableJson(item));
|
|
31
|
+
if (typeof value === "object") {
|
|
32
|
+
const source = value;
|
|
33
|
+
const sorted = {};
|
|
34
|
+
for (const key of Object.keys(source).sort()) {
|
|
35
|
+
const normalized = normalizeForStableJson(source[key]);
|
|
36
|
+
if (normalized !== undefined)
|
|
37
|
+
sorted[key] = normalized;
|
|
38
|
+
}
|
|
39
|
+
return sorted;
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function isEmptyPlainObject(value) {
|
|
44
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
45
|
+
}
|
|
46
|
+
export function buildQuerySubscriptionKey(queryKey, args) {
|
|
47
|
+
const normalized = normalizeForStableJson(args);
|
|
48
|
+
if (normalized === undefined || isEmptyPlainObject(normalized))
|
|
49
|
+
return queryKey;
|
|
50
|
+
return `${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}${JSON.stringify(normalized)}`;
|
|
51
|
+
}
|
|
52
|
+
export function subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey) {
|
|
53
|
+
return subscriptionKey === queryKey || subscriptionKey.startsWith(`${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}`);
|
|
54
|
+
}
|
|
21
55
|
// ─── Public API (Convex-style) ──────────────────────────
|
|
22
56
|
export function query(key, handlerOrDef) {
|
|
23
57
|
let handler;
|
|
@@ -167,6 +201,28 @@ export function deregisterClient(ws) {
|
|
|
167
201
|
clients.delete(ws);
|
|
168
202
|
}
|
|
169
203
|
}
|
|
204
|
+
function sendInvalidateToLocalSubscribers(queryKeys) {
|
|
205
|
+
const targets = new Map();
|
|
206
|
+
for (const queryKey of queryKeys) {
|
|
207
|
+
for (const [subscriptionKey, clients] of subscribers) {
|
|
208
|
+
if (!subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey))
|
|
209
|
+
continue;
|
|
210
|
+
for (const ws of clients) {
|
|
211
|
+
if (!targets.has(ws))
|
|
212
|
+
targets.set(ws, new Set());
|
|
213
|
+
targets.get(ws).add(subscriptionKey);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const [ws, keys] of targets) {
|
|
218
|
+
try {
|
|
219
|
+
ws.send(JSON.stringify({ type: "invalidate", queries: [...keys] }));
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
deregisterClient(ws);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
170
226
|
/**
|
|
171
227
|
* mutation 실행 시점에 생성되는 RealtimeCtx.
|
|
172
228
|
* emit(): 데이터를 직접 push (초고빈도 mutation용).
|
|
@@ -221,6 +277,15 @@ export function buildRealtimeCtx(options) {
|
|
|
221
277
|
}, 50); // 50ms batch window
|
|
222
278
|
pendingEmits.set(queryKey, { data, timer });
|
|
223
279
|
},
|
|
280
|
+
invalidate(queryKey) {
|
|
281
|
+
_hasEmitted = true;
|
|
282
|
+
const queryKeys = Array.isArray(queryKey) ? [...new Set(queryKey)] : [queryKey];
|
|
283
|
+
if (options?.httpCallback) {
|
|
284
|
+
options.httpCallback({ type: "invalidate", queryKeys });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
sendInvalidateToLocalSubscribers(queryKeys);
|
|
288
|
+
},
|
|
224
289
|
refresh(queryKey) {
|
|
225
290
|
_hasEmitted = true; // 경고 억제
|
|
226
291
|
if (!_pendingRefresh.includes(queryKey)) {
|
package/dist/rls-db.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { PgAsyncDatabase } from "drizzle-orm/pg-core";
|
|
2
1
|
/**
|
|
3
2
|
* RLS DB wrapper — execution paths for `withRlsConnection`:
|
|
4
3
|
* 1. **Reuse outer Drizzle transaction** (`reuseOuterConnection`): same connection, apply GUCs then run `fn`.
|
|
@@ -24,6 +23,13 @@ export type RlsSessionContext = {
|
|
|
24
23
|
*/
|
|
25
24
|
vars?: Record<string, string>;
|
|
26
25
|
};
|
|
26
|
+
type RlsDrizzleDatabaseLike = {
|
|
27
|
+
session: unknown;
|
|
28
|
+
_: {
|
|
29
|
+
session?: unknown;
|
|
30
|
+
};
|
|
31
|
+
transaction: (callback: (tx: unknown) => unknown | Promise<unknown>, ...rest: unknown[]) => Promise<unknown>;
|
|
32
|
+
};
|
|
27
33
|
/**
|
|
28
34
|
* pg `Pool.connect()`-style client: BEGIN → apply RLS GUCs → `fn` → COMMIT on success,
|
|
29
35
|
* or ROLLBACK if anything fails (including failed COMMIT). Always `release()` in `finally`.
|
|
@@ -44,4 +50,5 @@ export declare function withRlsLeasedConnection<T>(leased: {
|
|
|
44
50
|
*
|
|
45
51
|
* `db.transaction()` still injects the same variables at the start of the callback transaction.
|
|
46
52
|
*/
|
|
47
|
-
export declare function createRlsDb(db:
|
|
53
|
+
export declare function createRlsDb<TDb extends RlsDrizzleDatabaseLike>(db: TDb, rls: RlsSessionContext): TDb;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
const RESERVED_TENANT_RUNTIME_ENV_PREFIXES = ["__GENCOW_", "GENCOW_DOCUMENT_", "GENCOW_TEMPLATE_", "GENCOW_WARM_"];
|
|
40
|
+
export function isReservedTenantRuntimeEnvKey(key) {
|
|
41
|
+
const normalized = key.trim();
|
|
42
|
+
return (RESERVED_TENANT_RUNTIME_ENV_KEYS.has(normalized) ||
|
|
43
|
+
RESERVED_TENANT_RUNTIME_ENV_PREFIXES.some((prefix) => normalized.startsWith(prefix)));
|
|
44
|
+
}
|
|
45
|
+
export function filterTenantRuntimeEnvVars(vars) {
|
|
46
|
+
const allowed = {};
|
|
47
|
+
const rejectedKeys = [];
|
|
48
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
49
|
+
if (isReservedTenantRuntimeEnvKey(key)) {
|
|
50
|
+
rejectedKeys.push(key);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
allowed[key] = value;
|
|
54
|
+
}
|
|
55
|
+
return { allowed, rejectedKeys };
|
|
56
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type SearchPrimitive = string | number | boolean;
|
|
2
|
+
export type SearchScope = {
|
|
3
|
+
corpus: string;
|
|
4
|
+
visibility: "private" | "shared" | "public";
|
|
5
|
+
ownerUserId?: string;
|
|
6
|
+
};
|
|
7
|
+
export type SearchFilter = {
|
|
8
|
+
eq?: Record<string, SearchPrimitive>;
|
|
9
|
+
in?: Record<string, SearchPrimitive[]>;
|
|
10
|
+
range?: Record<string, {
|
|
11
|
+
gte?: string | number;
|
|
12
|
+
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
|
+
export type VectorSearchTuning = {
|
|
23
|
+
minScore?: number;
|
|
24
|
+
};
|
|
25
|
+
export type VectorSearchOptions = Omit<SearchOptions, "fields"> & {
|
|
26
|
+
vector: number[];
|
|
27
|
+
vectorField?: string;
|
|
28
|
+
tuning?: VectorSearchTuning;
|
|
29
|
+
};
|
|
30
|
+
export type HybridSearchFusionTuning = {
|
|
31
|
+
mode?: "rrf";
|
|
32
|
+
rrfK?: number;
|
|
33
|
+
keywordWeight?: number;
|
|
34
|
+
vectorWeight?: number;
|
|
35
|
+
};
|
|
36
|
+
export type HybridSearchTuning = {
|
|
37
|
+
keywordCandidateLimit?: number;
|
|
38
|
+
vectorCandidateLimit?: number;
|
|
39
|
+
minFusedScore?: number;
|
|
40
|
+
fusion?: HybridSearchFusionTuning;
|
|
41
|
+
};
|
|
42
|
+
export type HybridSearchOptions = SearchOptions & {
|
|
43
|
+
vector: number[];
|
|
44
|
+
vectorField?: string;
|
|
45
|
+
fusion?: "rrf";
|
|
46
|
+
keywordCandidateLimit?: number;
|
|
47
|
+
vectorCandidateLimit?: number;
|
|
48
|
+
tuning?: HybridSearchTuning;
|
|
49
|
+
};
|
|
50
|
+
export type SearchHit = {
|
|
51
|
+
id: string | number;
|
|
52
|
+
row: Record<string, unknown>;
|
|
53
|
+
score: number;
|
|
54
|
+
scores?: {
|
|
55
|
+
keyword?: number;
|
|
56
|
+
vector?: number;
|
|
57
|
+
fused?: number;
|
|
58
|
+
};
|
|
59
|
+
matchedBy: Array<"keyword" | "vector">;
|
|
60
|
+
};
|
|
61
|
+
export type SearchResponse = {
|
|
62
|
+
items: SearchHit[];
|
|
63
|
+
meta: {
|
|
64
|
+
engine: "tsvector" | "pgroonga";
|
|
65
|
+
tier: "free" | "pro" | "scale";
|
|
66
|
+
limit: number;
|
|
67
|
+
offset: number;
|
|
68
|
+
nextOffset?: number;
|
|
69
|
+
fusion?: "rrf";
|
|
70
|
+
mode?: "keyword" | "vector" | "hybrid";
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
export type SearchTierConfig = {
|
|
74
|
+
plan: "free" | "pro" | "scale";
|
|
75
|
+
engine: "tsvector" | "pgroonga";
|
|
76
|
+
hybridSearch: boolean;
|
|
77
|
+
locale: "english" | "multilingual";
|
|
78
|
+
extensions: {
|
|
79
|
+
vector: boolean;
|
|
80
|
+
pgroonga: boolean;
|
|
81
|
+
};
|
|
82
|
+
degradedReason?: string;
|
|
83
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/server.d.ts
CHANGED
|
@@ -6,6 +6,5 @@
|
|
|
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 { StorageImageTierConfig, StorageImageTransformMetric, StorageMeteringOptions, StoredFile, } from "./storage.js";
|
|
10
10
|
export { createScheduler, getSchedulerInfo } from "./scheduler.js";
|
|
11
|
-
export { authMiddleware, authRoutes, getUsers } from "./auth.js";
|
package/dist/server.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface StorageImageTransformMetric {
|
|
2
|
+
transformCount: number;
|
|
3
|
+
sourceBytes: number;
|
|
4
|
+
outputBytes: number;
|
|
5
|
+
format: string;
|
|
6
|
+
autoWebp: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface StorageMeteringOptions {
|
|
9
|
+
onImageTransform?: (metric: StorageImageTransformMetric) => void | Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function recordStorageImageTransform(options: StorageMeteringOptions | undefined, cachePath: string, metric: Omit<StorageImageTransformMetric, "transformCount" | "outputBytes">): Promise<{
|
|
12
|
+
size: number;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
export async function recordStorageImageTransform(options, cachePath, metric) {
|
|
3
|
+
const stats = await fs.stat(cachePath);
|
|
4
|
+
if (!options?.onImageTransform)
|
|
5
|
+
return stats;
|
|
6
|
+
try {
|
|
7
|
+
await options.onImageTransform({
|
|
8
|
+
...metric,
|
|
9
|
+
transformCount: 1,
|
|
10
|
+
outputBytes: stats.size,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
15
|
+
console.warn(`[storage] image transform metering failed: ${msg.slice(0, 120)}`);
|
|
16
|
+
}
|
|
17
|
+
return stats;
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
|
|
2
|
+
export declare const MAX_FILE_SIZE: number;
|
|
3
|
+
/** 기본 스토리지 쿼터: 1GB */
|
|
4
|
+
export declare const DEFAULT_STORAGE_QUOTA: number;
|
|
5
|
+
export interface StorageFile {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
size: number;
|
|
9
|
+
type: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
export interface StoredFile {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
size: number;
|
|
16
|
+
type: string;
|
|
17
|
+
buffer: Buffer;
|
|
18
|
+
}
|
|
19
|
+
export interface StorageOptions {
|
|
20
|
+
rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
21
|
+
storageQuota?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface Storage {
|
|
24
|
+
store(file: File | Blob, filename?: string): Promise<string>;
|
|
25
|
+
storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
|
|
26
|
+
get(storageId: string): Promise<StoredFile | null>;
|
|
27
|
+
getUrl(storageId: string): string;
|
|
28
|
+
getMeta(storageId: string): Promise<StorageFile | null>;
|
|
29
|
+
delete(storageId: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare function formatBytes(bytes: number): string;
|
|
32
|
+
export declare function loadStorageMeta(params: {
|
|
33
|
+
storageId: string;
|
|
34
|
+
dir: string;
|
|
35
|
+
metaStore: Map<string, StorageFile>;
|
|
36
|
+
}): Promise<StorageFile | null>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
/** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
|
|
4
|
+
export const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
5
|
+
/** 기본 스토리지 쿼터: 1GB */
|
|
6
|
+
export const DEFAULT_STORAGE_QUOTA = 1024 * 1024 * 1024;
|
|
7
|
+
export function formatBytes(bytes) {
|
|
8
|
+
if (bytes < 1024)
|
|
9
|
+
return `${bytes}B`;
|
|
10
|
+
if (bytes < 1024 * 1024)
|
|
11
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
12
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
13
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
14
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
15
|
+
}
|
|
16
|
+
export async function loadStorageMeta(params) {
|
|
17
|
+
const cached = params.metaStore.get(params.storageId);
|
|
18
|
+
if (cached) {
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
21
|
+
const filePath = path.join(params.dir, params.storageId);
|
|
22
|
+
try {
|
|
23
|
+
const raw = await fs.readFile(`${filePath}.meta`, "utf-8");
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
const stat = await fs.stat(filePath);
|
|
26
|
+
const meta = {
|
|
27
|
+
id: params.storageId,
|
|
28
|
+
name: typeof parsed.name === "string" ? parsed.name : params.storageId,
|
|
29
|
+
size: typeof parsed.size === "number" ? parsed.size : stat.size,
|
|
30
|
+
type: typeof parsed.type === "string" ? parsed.type : "application/octet-stream",
|
|
31
|
+
path: filePath,
|
|
32
|
+
};
|
|
33
|
+
params.metaStore.set(params.storageId, meta);
|
|
34
|
+
return meta;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type: string;
|
|
6
|
-
path: string;
|
|
7
|
-
}
|
|
8
|
-
export interface StorageOptions {
|
|
9
|
-
/** Raw SQL 실행 함수 — DB 자동 기록 + 쿼터 검증에 필요 */
|
|
10
|
-
rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
11
|
-
/** 앱별 스토리지 쿼터 (bytes). 0 = 무제한. 기본: 1GB */
|
|
12
|
-
storageQuota?: number;
|
|
13
|
-
}
|
|
14
|
-
export interface Storage {
|
|
15
|
-
/** Store a file and return a storageId — Convex의 ctx.storage.store() */
|
|
16
|
-
store(file: File | Blob, filename?: string): Promise<string>;
|
|
17
|
-
/** Store from raw buffer */
|
|
18
|
-
storeBuffer(buffer: Buffer, filename: string, type?: string): Promise<string>;
|
|
19
|
-
/** Get a serving URL for the file — Convex의 ctx.storage.getUrl() */
|
|
20
|
-
getUrl(storageId: string): string;
|
|
21
|
-
/** Get file metadata */
|
|
22
|
-
getMeta(storageId: string): Promise<StorageFile | null>;
|
|
23
|
-
/** Delete a stored file — Convex의 ctx.storage.delete() */
|
|
24
|
-
delete(storageId: string): Promise<void>;
|
|
25
|
-
}
|
|
1
|
+
import type { Storage, StorageOptions } from "./storage-shared.js";
|
|
2
|
+
import type { StorageMeteringOptions } from "./storage-metering.js";
|
|
3
|
+
export type { Storage, StorageFile, StorageOptions, StoredFile } from "./storage-shared.js";
|
|
4
|
+
export type { StorageImageTransformMetric, StorageMeteringOptions } from "./storage-metering.js";
|
|
26
5
|
/**
|
|
27
6
|
* Create a storage instance — Convex storage 패턴 재현
|
|
28
7
|
*
|
|
@@ -66,7 +45,7 @@ export interface StorageImageTierConfig {
|
|
|
66
45
|
* - 디스크 캐시: uploads/.cache/{uuid}_{params}.{ext}
|
|
67
46
|
* - 원본 보존: 원본 파일은 절대 수정하지 않음
|
|
68
47
|
*/
|
|
69
|
-
export declare function storageRoutes(storage: ReturnType<typeof createStorage>, rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>, storageDir?: string, tierConfig?: StorageImageTierConfig): (c: {
|
|
48
|
+
export declare function storageRoutes(storage: ReturnType<typeof createStorage>, rawSql?: (sql: string, params?: unknown[]) => Promise<unknown[]>, storageDir?: string, tierConfig?: StorageImageTierConfig, meteringOptions?: StorageMeteringOptions): (c: {
|
|
70
49
|
req: {
|
|
71
50
|
param: (key: string) => string;
|
|
72
51
|
query: (key: string) => string | undefined;
|
|
@@ -75,4 +54,3 @@ export declare function storageRoutes(storage: ReturnType<typeof createStorage>,
|
|
|
75
54
|
json: (data: unknown, status?: number) => Response;
|
|
76
55
|
body: (data: unknown, status: number, headers: Record<string, string>) => Response;
|
|
77
56
|
}) => Promise<Response>;
|
|
78
|
-
export {};
|
package/dist/storage.js
CHANGED
|
@@ -2,20 +2,8 @@ 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
|
-
|
|
7
|
-
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
8
|
-
/** 기본 스토리지 쿼터: 1GB */
|
|
9
|
-
const DEFAULT_STORAGE_QUOTA = 1024 * 1024 * 1024;
|
|
10
|
-
function formatBytes(bytes) {
|
|
11
|
-
if (bytes < 1024)
|
|
12
|
-
return `${bytes}B`;
|
|
13
|
-
if (bytes < 1024 * 1024)
|
|
14
|
-
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
15
|
-
if (bytes < 1024 * 1024 * 1024)
|
|
16
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
17
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
|
|
18
|
-
}
|
|
5
|
+
import { DEFAULT_STORAGE_QUOTA, MAX_FILE_SIZE, formatBytes, loadStorageMeta, } from "./storage-shared.js";
|
|
6
|
+
import { recordStorageImageTransform } from "./storage-metering.js";
|
|
19
7
|
// ─── Implementation ─────────────────────────────────────
|
|
20
8
|
const metaStore = new Map();
|
|
21
9
|
/**
|
|
@@ -55,6 +43,7 @@ async function ensureFilesTable(rawSql) {
|
|
|
55
43
|
async function checkStorageQuota(rawSql, newFileSize, quota) {
|
|
56
44
|
if (quota <= 0)
|
|
57
45
|
return; // 무제한
|
|
46
|
+
await ensureFilesTable(rawSql);
|
|
58
47
|
const rows = await rawSql(`SELECT COALESCE(SUM(CAST(size AS BIGINT)), 0) AS total FROM _system_files`);
|
|
59
48
|
const currentUsage = Number(rows[0]?.total || "0");
|
|
60
49
|
const projectedUsage = currentUsage + newFileSize;
|
|
@@ -166,8 +155,25 @@ export function createStorage(dir = "./uploads", options) {
|
|
|
166
155
|
getUrl(storageId) {
|
|
167
156
|
return `/api/storage/${storageId}`;
|
|
168
157
|
},
|
|
158
|
+
async get(storageId) {
|
|
159
|
+
const meta = await loadStorageMeta({ storageId, dir, metaStore });
|
|
160
|
+
if (!meta) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const buffer = await fs.readFile(meta.path).catch(() => null);
|
|
164
|
+
if (!buffer) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
id: meta.id,
|
|
169
|
+
name: meta.name,
|
|
170
|
+
size: meta.size,
|
|
171
|
+
type: meta.type,
|
|
172
|
+
buffer,
|
|
173
|
+
};
|
|
174
|
+
},
|
|
169
175
|
async getMeta(storageId) {
|
|
170
|
-
return
|
|
176
|
+
return loadStorageMeta({ storageId, dir, metaStore });
|
|
171
177
|
},
|
|
172
178
|
async delete(storageId) {
|
|
173
179
|
const meta = metaStore.get(storageId);
|
|
@@ -339,7 +345,7 @@ async function getCacheEntryCount(cacheDir) {
|
|
|
339
345
|
* - 디스크 캐시: uploads/.cache/{uuid}_{params}.{ext}
|
|
340
346
|
* - 원본 보존: 원본 파일은 절대 수정하지 않음
|
|
341
347
|
*/
|
|
342
|
-
export function storageRoutes(storage, rawSql, storageDir, tierConfig) {
|
|
348
|
+
export function storageRoutes(storage, rawSql, storageDir, tierConfig, meteringOptions) {
|
|
343
349
|
const baseTierConfig = {
|
|
344
350
|
autoWebp: tierConfig?.autoWebp ?? true,
|
|
345
351
|
autoMaxWidth: tierConfig?.autoMaxWidth ?? 0, // 0 = 제한 없음
|
|
@@ -557,14 +563,16 @@ export function storageRoutes(storage, rawSql, storageDir, tierConfig) {
|
|
|
557
563
|
}
|
|
558
564
|
// 변환 실행 → 캐시에 저장
|
|
559
565
|
await pipeline.toFile(cachePath);
|
|
566
|
+
const cacheStats = await recordStorageImageTransform(meteringOptions, cachePath, {
|
|
567
|
+
sourceBytes: meta.size,
|
|
568
|
+
format: outputFormat || meta.type.replace(/^image\//, ""),
|
|
569
|
+
autoWebp: isAutoWebp,
|
|
570
|
+
});
|
|
560
571
|
// WebP/AVIF가 원본보다 큰 경우 → 캐시 삭제 + 원본 서빙
|
|
561
572
|
// (Static Deploy와 동일 전략 — apps.ts L840-847)
|
|
562
|
-
if (isAutoWebp) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
await fs.unlink(cachePath).catch(() => { });
|
|
566
|
-
return serveOriginal(c, meta);
|
|
567
|
-
}
|
|
573
|
+
if (isAutoWebp && cacheStats.size >= meta.size) {
|
|
574
|
+
await fs.unlink(cachePath).catch(() => { });
|
|
575
|
+
return serveOriginal(c, meta);
|
|
568
576
|
}
|
|
569
577
|
return serveCachedFile(c, cachePath, transformParams, isAutoWebp, meta);
|
|
570
578
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type WakeAppSuccessStatus = "already_running" | "woke";
|
|
2
|
+
export type WakeAppDeferredStatus = "capacity_rejected" | "queue_timeout";
|
|
3
|
+
export type WakeAppSuccessResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
status: WakeAppSuccessStatus;
|
|
6
|
+
port: number;
|
|
7
|
+
};
|
|
8
|
+
export type WakeAppDeferredResult = {
|
|
9
|
+
ok: false;
|
|
10
|
+
status: WakeAppDeferredStatus;
|
|
11
|
+
retryAfterSec: number;
|
|
12
|
+
};
|
|
13
|
+
export type WakeAppBootFailedResult = {
|
|
14
|
+
ok: false;
|
|
15
|
+
status: "boot_failed";
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
18
|
+
export type WakeAppResult = WakeAppSuccessResult | WakeAppDeferredResult | WakeAppBootFailedResult;
|
|
19
|
+
export declare const DEFAULT_WAKE_RETRY_AFTER_SEC = 30;
|
|
20
|
+
export declare function buildWakeAppSuccessResult(status: WakeAppSuccessStatus, port: number): WakeAppSuccessResult;
|
|
21
|
+
export declare function buildWakeAppBootFailedResult(error: unknown): WakeAppBootFailedResult;
|
|
22
|
+
export declare function isWakeAppDeferredResult(result: WakeAppResult): result is WakeAppDeferredResult;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEFAULT_WAKE_RETRY_AFTER_SEC = 30;
|
|
2
|
+
export function buildWakeAppSuccessResult(status, port) {
|
|
3
|
+
return { ok: true, status, port };
|
|
4
|
+
}
|
|
5
|
+
export function buildWakeAppBootFailedResult(error) {
|
|
6
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
+
return { ok: false, status: "boot_failed", error: message };
|
|
8
|
+
}
|
|
9
|
+
export function isWakeAppDeferredResult(result) {
|
|
10
|
+
return !result.ok && (result.status === "capacity_rejected" || result.status === "queue_timeout");
|
|
11
|
+
}
|
package/dist/workflow-types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { GencowCtx } from "./
|
|
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
|
export type WorkflowStatus = "pending" | "running" | "completed" | "failed";
|
|
4
5
|
export type WorkflowDuration = number | string;
|
|
@@ -11,6 +12,7 @@ export interface WorkflowSummary {
|
|
|
11
12
|
derivedStatus: WorkflowDerivedStatus;
|
|
12
13
|
currentStep: string | null;
|
|
13
14
|
error: string | null;
|
|
15
|
+
errorCode: string | null;
|
|
14
16
|
retryCount: number;
|
|
15
17
|
maxRetries: number;
|
|
16
18
|
maxDurationMs: number;
|
|
@@ -53,9 +55,13 @@ export interface WorkflowSignalResult {
|
|
|
53
55
|
export interface WorkflowResumePayload {
|
|
54
56
|
workflowId: string;
|
|
55
57
|
}
|
|
56
|
-
export interface WorkflowCtx extends GencowCtx {
|
|
58
|
+
export interface WorkflowCtx extends Omit<GencowCtx, "services"> {
|
|
57
59
|
workflowId: string;
|
|
58
60
|
workflowName: string;
|
|
61
|
+
runId?: string;
|
|
62
|
+
attempt?: number;
|
|
63
|
+
stepAttempt?: number;
|
|
64
|
+
services: GencowCtx["services"] & WorkflowDocumentServicesCtx;
|
|
59
65
|
step<TResult>(name: string, run: () => Promise<TResult>): Promise<TResult>;
|
|
60
66
|
sleep(duration: WorkflowDuration): Promise<void>;
|
|
61
67
|
waitForEvent<TPayload = unknown>(name: string, timeout?: WorkflowDuration): Promise<TPayload>;
|
|
@@ -67,7 +73,11 @@ export type WorkflowHandler<TArgs, TReturn> = (wf: WorkflowCtx, args: TArgs) =>
|
|
|
67
73
|
export interface WorkflowOptions<TSchema = any, TReturn = any> {
|
|
68
74
|
args?: TSchema;
|
|
69
75
|
public?: boolean;
|
|
76
|
+
version?: string;
|
|
70
77
|
maxDuration?: WorkflowDuration;
|
|
78
|
+
maxActiveDuration?: WorkflowDuration;
|
|
79
|
+
lifecycleTimeout?: WorkflowDuration;
|
|
80
|
+
concurrency?: number;
|
|
71
81
|
retries?: number;
|
|
72
82
|
handler: WorkflowHandler<InferArgs<TSchema>, TReturn>;
|
|
73
83
|
}
|
|
@@ -75,7 +85,11 @@ export interface WorkflowDef<TSchema = any, TReturn = any> {
|
|
|
75
85
|
name: string;
|
|
76
86
|
argsSchema?: TSchema;
|
|
77
87
|
isPublic: boolean;
|
|
88
|
+
version?: string;
|
|
78
89
|
maxDurationMs: number;
|
|
90
|
+
maxActiveDurationMs: number;
|
|
91
|
+
lifecycleTimeoutMs: number | null;
|
|
92
|
+
concurrency: number | null;
|
|
79
93
|
maxRetries: number;
|
|
80
94
|
handler: WorkflowHandler<InferArgs<TSchema>, TReturn>;
|
|
81
95
|
}
|
package/dist/workflow.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MutationDef } from "./reactive.js";
|
|
1
|
+
import type { MutationDef } from "./reactive-mutation-types.js";
|
|
2
2
|
import type { WorkflowDef, WorkflowDuration, WorkflowOptions, WorkflowStartResult } from "./workflow-types.js";
|
|
3
3
|
declare global {
|
|
4
4
|
var __gencow_workflowRegistry: Map<string, WorkflowDef<any, any>>;
|