@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,87 @@
1
+ import { bigint, boolean, doublePrecision, integer, jsonb, pgTable, text, timestamp, vector } from "drizzle-orm/pg-core";
2
+ function buildScopeColumns() {
3
+ return {
4
+ id: text("id").primaryKey(),
5
+ corpus: text("corpus").notNull(),
6
+ visibilityScope: text("visibility_scope").notNull(),
7
+ ownerUserId: text("owner_user_id"),
8
+ createdAt: timestamp("created_at").defaultNow().notNull(),
9
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
10
+ };
11
+ }
12
+ export const ragCorpora = pgTable("rag_corpora", {
13
+ ...buildScopeColumns(),
14
+ title: text("title").notNull(),
15
+ description: text("description"),
16
+ allowExternalProviders: boolean("allow_external_providers").default(false).notNull(),
17
+ retentionDays: integer("retention_days"),
18
+ metadata: jsonb("metadata").default({}).notNull(),
19
+ });
20
+ export const ragSources = pgTable("rag_sources", {
21
+ ...buildScopeColumns(),
22
+ storageId: text("storage_id").notNull(),
23
+ sourceKey: text("source_key").notNull(),
24
+ sourceTitle: text("source_title").notNull(),
25
+ mimeType: text("mime_type").notNull(),
26
+ byteSize: bigint("byte_size", { mode: "number" }).notNull(),
27
+ sourceChecksum: text("source_checksum").notNull(),
28
+ convertProvider: text("convert_provider").notNull(),
29
+ convertStatus: text("convert_status").notNull(),
30
+ markdown: text("markdown").notNull(),
31
+ text: text("text").notNull(),
32
+ warnings: jsonb("warnings").default([]).notNull(),
33
+ metadata: jsonb("metadata").default({}).notNull(),
34
+ });
35
+ export const ragSections = pgTable("rag_sections", {
36
+ ...buildScopeColumns(),
37
+ sourceId: text("source_id").notNull(),
38
+ sectionIndex: integer("section_index").notNull(),
39
+ sectionPath: jsonb("section_path").default([]).notNull(),
40
+ title: text("title"),
41
+ depth: integer("depth").notNull(),
42
+ pageStart: integer("page_start"),
43
+ pageEnd: integer("page_end"),
44
+ charStart: integer("char_start").notNull(),
45
+ charEnd: integer("char_end").notNull(),
46
+ });
47
+ export const ragChunks = pgTable("rag_chunks", {
48
+ ...buildScopeColumns(),
49
+ sourceId: text("source_id").notNull(),
50
+ sectionId: text("section_id"),
51
+ chunkIndex: integer("chunk_index").notNull(),
52
+ chunkText: text("chunk_text").notNull(),
53
+ lexicalText: text("lexical_text").notNull(),
54
+ embedding: vector("embedding", { dimensions: 1536 }),
55
+ pageStart: integer("page_start"),
56
+ pageEnd: integer("page_end"),
57
+ chunkChecksum: text("chunk_checksum").notNull(),
58
+ metadata: jsonb("metadata").default({}).notNull(),
59
+ });
60
+ export const ragIngestJobs = pgTable("rag_ingest_jobs", {
61
+ ...buildScopeColumns(),
62
+ workflowId: text("workflow_id").notNull(),
63
+ sourceId: text("source_id").notNull(),
64
+ status: text("status").notNull(),
65
+ stage: text("stage").notNull(),
66
+ providerTrace: jsonb("provider_trace").default({}).notNull(),
67
+ metrics: jsonb("metrics").default({}).notNull(),
68
+ error: text("error"),
69
+ startedAt: timestamp("started_at").defaultNow().notNull(),
70
+ completedAt: timestamp("completed_at"),
71
+ });
72
+ export const ragOperationMetrics = pgTable("rag_operation_metrics", {
73
+ id: text("id").primaryKey(),
74
+ appId: text("app_id").notNull(),
75
+ corpus: text("corpus").notNull(),
76
+ visibilityScope: text("visibility_scope").notNull(),
77
+ ownerUserId: text("owner_user_id"),
78
+ operation: text("operation").notNull(),
79
+ jobId: text("job_id"),
80
+ workflowId: text("workflow_id"),
81
+ sourceId: text("source_id"),
82
+ metricName: text("metric_name").notNull(),
83
+ metricValue: doublePrecision("metric_value").notNull(),
84
+ unit: text("unit"),
85
+ metadata: jsonb("metadata").default({}).notNull(),
86
+ recordedAt: timestamp("recorded_at").defaultNow().notNull(),
87
+ });
@@ -0,0 +1,11 @@
1
+ import type { InferArgs } from "./v.js";
2
+ import type { GencowCtx } from "./context.js";
3
+ export type MutationHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
4
+ export interface MutationDef<TSchema = any, TReturn = any> {
5
+ handler: MutationHandler<InferArgs<TSchema>, TReturn>;
6
+ argsSchema?: TSchema;
7
+ /** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
8
+ isPublic: boolean;
9
+ _args?: InferArgs<TSchema>;
10
+ _return?: TReturn;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { type InferArgs } from "./v.js";
2
+ import type { MutationDef, MutationHandler } from "./reactive-mutation-types.js";
3
+ declare global {
4
+ var __gencow_mutationRegistry: (MutationDef<any, any> & {
5
+ name: string;
6
+ })[];
7
+ }
8
+ export declare const mutationRegistry: (MutationDef<any, any> & {
9
+ name: string;
10
+ })[];
11
+ /**
12
+ * mutation — 데이터 변경 함수를 선언적으로 등록합니다.
13
+ *
14
+ * 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // ✅ 권장: query와 동일한 (name, def) 패턴
19
+ * mutation("tasks.create", {
20
+ * args: { title: v.string() },
21
+ * handler: async (ctx, args) => { ... },
22
+ * });
23
+ *
24
+ * // ✅ 객체 스타일 (하위 호환)
25
+ * mutation({
26
+ * name: "tasks.create",
27
+ * handler: async (ctx) => { ... },
28
+ * });
29
+ *
30
+ * // ⚠️ Legacy 배열 스타일 (비권장)
31
+ * mutation(["tasks.list"], handler, "tasks.create");
32
+ * ```
33
+ *
34
+ * @note invalidates 필드는 deprecated — 전달해도 무시됩니다.
35
+ * 리얼타임 UI 갱신에는 ctx.realtime.emit() 또는 ctx.realtime.refresh()를 사용하세요.
36
+ */
37
+ export declare function mutation<TSchema = any, TReturn = any>(nameOrInvalidatesOrDef: string | string[] | {
38
+ name?: string;
39
+ args?: TSchema;
40
+ public?: boolean;
41
+ invalidates?: string[];
42
+ handler: MutationHandler<InferArgs<TSchema>, TReturn>;
43
+ }, handlerOrDef?: MutationHandler<InferArgs<TSchema>, TReturn> | {
44
+ invalidates?: string[];
45
+ args?: TSchema;
46
+ public?: boolean;
47
+ handler: MutationHandler<InferArgs<TSchema>, TReturn>;
48
+ }, name?: string): MutationDef<TSchema, TReturn>;
49
+ export declare function getRegisteredMutations(): (MutationDef & {
50
+ name: string;
51
+ })[];
@@ -0,0 +1,75 @@
1
+ if (!globalThis.__gencow_mutationRegistry)
2
+ globalThis.__gencow_mutationRegistry = [];
3
+ export const mutationRegistry = globalThis.__gencow_mutationRegistry;
4
+ let mutationCounter = 0;
5
+ /**
6
+ * mutation — 데이터 변경 함수를 선언적으로 등록합니다.
7
+ *
8
+ * 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // ✅ 권장: query와 동일한 (name, def) 패턴
13
+ * mutation("tasks.create", {
14
+ * args: { title: v.string() },
15
+ * handler: async (ctx, args) => { ... },
16
+ * });
17
+ *
18
+ * // ✅ 객체 스타일 (하위 호환)
19
+ * mutation({
20
+ * name: "tasks.create",
21
+ * handler: async (ctx) => { ... },
22
+ * });
23
+ *
24
+ * // ⚠️ Legacy 배열 스타일 (비권장)
25
+ * mutation(["tasks.list"], handler, "tasks.create");
26
+ * ```
27
+ *
28
+ * @note invalidates 필드는 deprecated — 전달해도 무시됩니다.
29
+ * 리얼타임 UI 갱신에는 ctx.realtime.emit() 또는 ctx.realtime.refresh()를 사용하세요.
30
+ */
31
+ export function mutation(nameOrInvalidatesOrDef, handlerOrDef, name) {
32
+ let argsSchema;
33
+ let actualHandler;
34
+ let mutName;
35
+ let isPublic = false;
36
+ if (typeof nameOrInvalidatesOrDef === "string") {
37
+ // New primary style: mutation("name", { args?, public?, handler })
38
+ mutName = nameOrInvalidatesOrDef;
39
+ const def = handlerOrDef;
40
+ actualHandler = def.handler;
41
+ argsSchema = def.args;
42
+ isPublic = def.public === true;
43
+ }
44
+ else if (Array.isArray(nameOrInvalidatesOrDef)) {
45
+ // Legacy style: mutation([...], handler, "name") — invalidates ignored
46
+ actualHandler = handlerOrDef;
47
+ mutName = name || `mutation_${++mutationCounter}`;
48
+ }
49
+ else {
50
+ // Object style: mutation({ name?, args?, public?, handler })
51
+ actualHandler = nameOrInvalidatesOrDef.handler;
52
+ argsSchema = nameOrInvalidatesOrDef.args;
53
+ isPublic = nameOrInvalidatesOrDef.public === true;
54
+ mutName =
55
+ nameOrInvalidatesOrDef.name ||
56
+ (typeof name === "string" ? name : "") ||
57
+ `mutation_${++mutationCounter}`;
58
+ }
59
+ // 이름 미지정 시 경고 — 디버깅 지원
60
+ if (mutName.startsWith("mutation_")) {
61
+ console.warn(`[gencow] mutation registered without explicit name → "${mutName}". ` +
62
+ `Use mutation("myMutation", { handler }) for better debugging.`);
63
+ }
64
+ const def = {
65
+ name: mutName,
66
+ handler: actualHandler,
67
+ argsSchema,
68
+ isPublic,
69
+ };
70
+ mutationRegistry.push(def);
71
+ return def;
72
+ }
73
+ export function getRegisteredMutations() {
74
+ return [...mutationRegistry];
75
+ }
@@ -0,0 +1,12 @@
1
+ import type { InferArgs } from "./v.js";
2
+ import type { GencowCtx } from "./context.js";
3
+ export type QueryHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
4
+ export interface QueryDef<TSchema = any, TReturn = any> {
5
+ key: string;
6
+ handler: QueryHandler<InferArgs<TSchema>, TReturn>;
7
+ argsSchema?: TSchema;
8
+ /** true = 인증 없이 접근 가능, false(기본) = auth 필수 (Secure by Default) */
9
+ isPublic: boolean;
10
+ _args?: InferArgs<TSchema>;
11
+ _return?: TReturn;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { type InferArgs } from "./v.js";
2
+ import type { QueryDef, QueryHandler } from "./reactive-query-types.js";
3
+ declare global {
4
+ var __gencow_queryRegistry: Map<string, QueryDef<any, any>>;
5
+ }
6
+ export declare const queryRegistry: Map<string, QueryDef<any, any>>;
7
+ export declare function query<TSchema = any, TReturn = any>(key: string, handlerOrDef: QueryHandler<InferArgs<TSchema>, TReturn> | {
8
+ args?: TSchema;
9
+ public?: boolean;
10
+ handler: QueryHandler<InferArgs<TSchema>, TReturn>;
11
+ }): QueryDef<TSchema, TReturn>;
12
+ export declare function getQueryHandler(key: string): QueryHandler | undefined;
13
+ export declare function getQueryDef(key: string): QueryDef | undefined;
14
+ export declare function getRegisteredQueries(): string[];
@@ -0,0 +1,28 @@
1
+ if (!globalThis.__gencow_queryRegistry)
2
+ globalThis.__gencow_queryRegistry = new Map();
3
+ export const queryRegistry = globalThis.__gencow_queryRegistry;
4
+ export function query(key, handlerOrDef) {
5
+ let handler;
6
+ let argsSchema;
7
+ let isPublic = false;
8
+ if (typeof handlerOrDef === "function") {
9
+ handler = handlerOrDef;
10
+ }
11
+ else {
12
+ handler = handlerOrDef.handler;
13
+ argsSchema = handlerOrDef.args;
14
+ isPublic = handlerOrDef.public === true;
15
+ }
16
+ const def = { key, handler, argsSchema, isPublic };
17
+ queryRegistry.set(key, def);
18
+ return def;
19
+ }
20
+ export function getQueryHandler(key) {
21
+ return queryRegistry.get(key)?.handler;
22
+ }
23
+ export function getQueryDef(key) {
24
+ return queryRegistry.get(key);
25
+ }
26
+ export function getRegisteredQueries() {
27
+ return Array.from(queryRegistry.keys());
28
+ }
@@ -0,0 +1,48 @@
1
+ import type { WSContext } from "hono/ws";
2
+ import type { GencowCtx, RealtimeCtx, RealtimeNotifyEvent } from "./context.js";
3
+ import type { QueryDef } from "./reactive-query-types.js";
4
+ declare global {
5
+ var __gencow_subscribers: Map<string, Set<WSContext>>;
6
+ var __gencow_connectedClients: Set<WSContext>;
7
+ }
8
+ export declare const subscribers: Map<string, Set<WSContext<unknown>>>;
9
+ /**
10
+ * Every WebSocket client that ever establishes a connection is tracked here.
11
+ * This allows broadcasting invalidation events to clients that never subscribed
12
+ * to a specific query (e.g. the admin dashboard's raw WebSocket connection).
13
+ */
14
+ export declare const connectedClients: Set<WSContext<unknown>>;
15
+ /** Stable subscription key for WebSocket / invalidate fanout (also used by crud for query keys). */
16
+ export declare function buildQuerySubscriptionKey(queryKey: string, args?: unknown): string;
17
+ export declare function subscriptionKeyMatchesQueryKey(subscriptionKey: string, queryKey: string): boolean;
18
+ export declare function subscribe(queryKey: string, ws: WSContext): void;
19
+ export declare function unsubscribe(ws: WSContext): void;
20
+ /** Register a raw WS connection without any query subscription (e.g. admin dashboard) */
21
+ export declare function registerClient(ws: WSContext): void;
22
+ export declare function deregisterClient(ws: WSContext): void;
23
+ /**
24
+ * mutation 실행 시점에 생성되는 RealtimeCtx.
25
+ * emit(): 데이터를 직접 push (초고빈도 mutation용).
26
+ * refresh(): queryKey를 pending 큐에 추가, mutation 완료 후 서버가 query re-run하여 push.
27
+ *
28
+ * 💡 Batching: 같은 queryKey에 대한 emit이 50ms 내에 여러 번 호출되면
29
+ * 마지막 데이터만 push하여 불필요한 전송을 방지합니다.
30
+ *
31
+ * ⚠️ 매 mutation 호출마다 새로 생성해야 합니다 (debounce timer가 mutation scope에 격리).
32
+ *
33
+ * @param options.httpCallback BaaS 모드: Platform WS Gateway에 HTTP로 emit 전달.
34
+ * 설정되면 WS 직접 push 대신 이 콜백을 호출.
35
+ * 로컬 dev에서는 미설정 → 기존 WS 직접 push 유지.
36
+ * @param options.queryMap query 레지스트리 — refresh()에서 query handler를 찾아 re-run.
37
+ * @param options.buildCtxForRefresh refresh 시 query handler에 전달할 ctx 생성 함수.
38
+ */
39
+ export declare function buildRealtimeCtx(options?: {
40
+ httpCallback?: (event: RealtimeNotifyEvent) => void;
41
+ queryMap?: Map<string, QueryDef<any, any>>;
42
+ buildCtxForRefresh?: () => GencowCtx;
43
+ }): RealtimeCtx & {
44
+ _hasEmitted: boolean;
45
+ _pendingRefresh: string[];
46
+ _flushRefresh: () => Promise<void>;
47
+ };
48
+ export declare function handleWsMessage(ws: WSContext, raw: string | ArrayBuffer): void;
@@ -0,0 +1,236 @@
1
+ import { queryRegistry } from "./reactive-query.js";
2
+ if (!globalThis.__gencow_subscribers)
3
+ globalThis.__gencow_subscribers = new Map();
4
+ if (!globalThis.__gencow_connectedClients)
5
+ globalThis.__gencow_connectedClients = new Set();
6
+ export const subscribers = globalThis.__gencow_subscribers;
7
+ /**
8
+ * Every WebSocket client that ever establishes a connection is tracked here.
9
+ * This allows broadcasting invalidation events to clients that never subscribed
10
+ * to a specific query (e.g. the admin dashboard's raw WebSocket connection).
11
+ */
12
+ export const connectedClients = globalThis.__gencow_connectedClients;
13
+ const SUBSCRIPTION_KEY_SEPARATOR = "::";
14
+ function normalizeForStableJson(value) {
15
+ if (value === undefined)
16
+ return undefined;
17
+ if (value === null)
18
+ return null;
19
+ if (value instanceof Date)
20
+ return value.toISOString();
21
+ if (Array.isArray(value))
22
+ return value.map((item) => normalizeForStableJson(item));
23
+ if (typeof value === "object") {
24
+ const source = value;
25
+ const sorted = {};
26
+ for (const key of Object.keys(source).sort()) {
27
+ const normalized = normalizeForStableJson(source[key]);
28
+ if (normalized !== undefined)
29
+ sorted[key] = normalized;
30
+ }
31
+ return sorted;
32
+ }
33
+ return value;
34
+ }
35
+ function isEmptyPlainObject(value) {
36
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
37
+ }
38
+ /** Stable subscription key for WebSocket / invalidate fanout (also used by crud for query keys). */
39
+ export function buildQuerySubscriptionKey(queryKey, args) {
40
+ const normalized = normalizeForStableJson(args);
41
+ if (normalized === undefined || isEmptyPlainObject(normalized))
42
+ return queryKey;
43
+ return `${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}${JSON.stringify(normalized)}`;
44
+ }
45
+ export function subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey) {
46
+ return subscriptionKey === queryKey || subscriptionKey.startsWith(`${queryKey}${SUBSCRIPTION_KEY_SEPARATOR}`);
47
+ }
48
+ export function subscribe(queryKey, ws) {
49
+ connectedClients.add(ws);
50
+ if (!subscribers.has(queryKey)) {
51
+ subscribers.set(queryKey, new Set());
52
+ }
53
+ subscribers.get(queryKey).add(ws);
54
+ }
55
+ export function unsubscribe(ws) {
56
+ connectedClients.delete(ws);
57
+ for (const clients of subscribers.values()) {
58
+ clients.delete(ws);
59
+ }
60
+ }
61
+ /** Register a raw WS connection without any query subscription (e.g. admin dashboard) */
62
+ export function registerClient(ws) {
63
+ connectedClients.add(ws);
64
+ }
65
+ export function deregisterClient(ws) {
66
+ connectedClients.delete(ws);
67
+ for (const clients of subscribers.values()) {
68
+ clients.delete(ws);
69
+ }
70
+ }
71
+ function sendInvalidateToLocalSubscribers(queryKeys) {
72
+ const targets = new Map();
73
+ for (const queryKey of queryKeys) {
74
+ for (const [subscriptionKey, clients] of subscribers) {
75
+ if (!subscriptionKeyMatchesQueryKey(subscriptionKey, queryKey))
76
+ continue;
77
+ for (const ws of clients) {
78
+ if (!targets.has(ws))
79
+ targets.set(ws, new Set());
80
+ targets.get(ws).add(subscriptionKey);
81
+ }
82
+ }
83
+ }
84
+ for (const [ws, keys] of targets) {
85
+ try {
86
+ ws.send(JSON.stringify({ type: "invalidate", queries: [...keys] }));
87
+ }
88
+ catch {
89
+ deregisterClient(ws);
90
+ }
91
+ }
92
+ }
93
+ /**
94
+ * mutation 실행 시점에 생성되는 RealtimeCtx.
95
+ * emit(): 데이터를 직접 push (초고빈도 mutation용).
96
+ * refresh(): queryKey를 pending 큐에 추가, mutation 완료 후 서버가 query re-run하여 push.
97
+ *
98
+ * 💡 Batching: 같은 queryKey에 대한 emit이 50ms 내에 여러 번 호출되면
99
+ * 마지막 데이터만 push하여 불필요한 전송을 방지합니다.
100
+ *
101
+ * ⚠️ 매 mutation 호출마다 새로 생성해야 합니다 (debounce timer가 mutation scope에 격리).
102
+ *
103
+ * @param options.httpCallback BaaS 모드: Platform WS Gateway에 HTTP로 emit 전달.
104
+ * 설정되면 WS 직접 push 대신 이 콜백을 호출.
105
+ * 로컬 dev에서는 미설정 → 기존 WS 직접 push 유지.
106
+ * @param options.queryMap query 레지스트리 — refresh()에서 query handler를 찾아 re-run.
107
+ * @param options.buildCtxForRefresh refresh 시 query handler에 전달할 ctx 생성 함수.
108
+ */
109
+ export function buildRealtimeCtx(options) {
110
+ const pendingEmits = new Map();
111
+ const _pendingRefresh = [];
112
+ let _hasEmitted = false;
113
+ return {
114
+ emit(queryKey, data) {
115
+ _hasEmitted = true;
116
+ // 기존 pending timer가 있으면 취소 (debounce)
117
+ const existing = pendingEmits.get(queryKey);
118
+ if (existing)
119
+ clearTimeout(existing.timer);
120
+ const timer = setTimeout(() => {
121
+ pendingEmits.delete(queryKey);
122
+ // BaaS 모드: Platform WS Gateway에 HTTP callback
123
+ if (options?.httpCallback) {
124
+ options.httpCallback({ type: "emit", queryKey, data });
125
+ return;
126
+ }
127
+ // 로컬 dev: WS 직접 push (기존 동작)
128
+ const clients = subscribers.get(queryKey);
129
+ if (!clients || clients.size === 0)
130
+ return;
131
+ const message = JSON.stringify({
132
+ type: "query:updated",
133
+ query: queryKey,
134
+ data,
135
+ });
136
+ for (const ws of clients) {
137
+ try {
138
+ ws.send(message);
139
+ }
140
+ catch {
141
+ clients.delete(ws);
142
+ }
143
+ }
144
+ }, 50); // 50ms batch window
145
+ pendingEmits.set(queryKey, { data, timer });
146
+ },
147
+ invalidate(queryKey) {
148
+ _hasEmitted = true;
149
+ const queryKeys = Array.isArray(queryKey) ? [...new Set(queryKey)] : [queryKey];
150
+ if (options?.httpCallback) {
151
+ options.httpCallback({ type: "invalidate", queryKeys });
152
+ return;
153
+ }
154
+ sendInvalidateToLocalSubscribers(queryKeys);
155
+ },
156
+ refresh(queryKey) {
157
+ _hasEmitted = true; // 경고 억제
158
+ if (!_pendingRefresh.includes(queryKey)) {
159
+ _pendingRefresh.push(queryKey);
160
+ }
161
+ },
162
+ get _hasEmitted() {
163
+ return _hasEmitted;
164
+ },
165
+ get _pendingRefresh() {
166
+ return [..._pendingRefresh];
167
+ },
168
+ async _flushRefresh() {
169
+ if (_pendingRefresh.length === 0)
170
+ return;
171
+ // queryMap이 없으면 refresh 동작 불가 (로그 경고)
172
+ const qMap = options?.queryMap ?? queryRegistry;
173
+ for (const key of _pendingRefresh) {
174
+ const queryDef = qMap.get(key);
175
+ if (!queryDef) {
176
+ console.warn(`[gencow] refresh("${key}"): query not found in registry. Skipping.`);
177
+ continue;
178
+ }
179
+ try {
180
+ // refresh용 ctx 생성 (mutation ctx와 동일한 DB/auth 스코프)
181
+ if (!options?.buildCtxForRefresh) {
182
+ console.warn(`[gencow] ⚠️ refresh("${key}"): buildCtxForRefresh not provided. ` +
183
+ `Query handler will receive an empty ctx — ctx.db will be undefined. ` +
184
+ `This is a framework configuration error. ` +
185
+ `💡 Ensure buildRealtimeCtx() receives a buildCtxForRefresh callback.`);
186
+ }
187
+ const refreshCtx = options?.buildCtxForRefresh?.() ?? {};
188
+ const result = await queryDef.handler(refreshCtx, {});
189
+ // emit과 동일한 경로로 push
190
+ if (options?.httpCallback) {
191
+ options.httpCallback({ type: "emit", queryKey: key, data: result });
192
+ }
193
+ else {
194
+ const clients = subscribers.get(key);
195
+ if (clients && clients.size > 0) {
196
+ const message = JSON.stringify({
197
+ type: "query:updated",
198
+ query: key,
199
+ data: result,
200
+ });
201
+ for (const ws of clients) {
202
+ try {
203
+ ws.send(message);
204
+ }
205
+ catch {
206
+ clients.delete(ws);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ catch (e) {
213
+ console.warn(`[gencow] refresh("${key}") failed:`, e instanceof Error ? e.message : e);
214
+ }
215
+ }
216
+ _pendingRefresh.length = 0;
217
+ },
218
+ };
219
+ }
220
+ export function handleWsMessage(ws, raw) {
221
+ try {
222
+ const msg = typeof raw === "string" ? JSON.parse(raw) : JSON.parse(raw.toString());
223
+ if (msg.type === "subscribe" && msg.query) {
224
+ subscribe(msg.query, ws);
225
+ ws.send(JSON.stringify({ type: "subscribed", query: msg.query }));
226
+ }
227
+ if (msg.type === "unsubscribe" && msg.query) {
228
+ const clients = subscribers.get(msg.query);
229
+ if (clients)
230
+ clients.delete(ws);
231
+ }
232
+ }
233
+ catch {
234
+ // ignore malformed messages
235
+ }
236
+ }
@@ -2,6 +2,9 @@ import type { WSContext } from "hono/ws";
2
2
  import type { Storage } from "./storage.js";
3
3
  import type { Scheduler } from "./scheduler.js";
4
4
  import { type InferArgs } from "./v.js";
5
+ import type { HybridSearchOptions, SearchOptions, SearchResponse, VectorSearchOptions } from "./search-types.js";
6
+ import type { GencowServicesCtx } from "./document-types.js";
7
+ import type { GroundingRuntime } from "./grounded-answer-types.js";
5
8
  export interface UserIdentity {
6
9
  id: string;
7
10
  email: string;
@@ -30,6 +33,11 @@ export interface RealtimeCtx {
30
33
  * ctx.realtime.emit("tasks.list", freshList);
31
34
  */
32
35
  emit(queryKey: string, data: unknown): void;
36
+ /**
37
+ * 특정 queryKey 구독자에게 재조회 신호만 보냅니다.
38
+ * private/RLS query처럼 서버가 안전하게 full payload를 만들 수 없는 경우 기본값입니다.
39
+ */
40
+ invalidate(queryKey: string | string[]): void;
33
41
  /**
34
42
  * 수동 mutation에서 리얼타임 업데이트의 기본 권장 방식.
35
43
  * mutation handler 완료 후 서버가 해당 query를 re-run하여 결과를 push합니다.
@@ -48,6 +56,14 @@ export interface RealtimeCtx {
48
56
  */
49
57
  refresh(queryKey: string): void;
50
58
  }
59
+ export type RealtimeNotifyEvent = {
60
+ type: "emit";
61
+ queryKey: string;
62
+ data: unknown;
63
+ } | {
64
+ type: "invalidate";
65
+ queryKeys: string[];
66
+ };
51
67
  /**
52
68
  * 사용자 함수(query/mutation)에 주입되는 컨텍스트.
53
69
  * Convex의 ctx 패턴과 동일하게, 이 객체를 통해서만 DB/Storage/Auth에 접근 가능.
@@ -101,8 +117,18 @@ export interface GencowCtx {
101
117
  realtime: RealtimeCtx;
102
118
  /** 재시도 — ctx.retry(fn, opts) — exponential backoff + jitter */
103
119
  retry: <T>(fn: () => Promise<T>, options?: import("./retry.js").RetryOptions) => Promise<T>;
120
+ /** 프레임워크 서비스 헬퍼 — workflow 전용 service는 별도 ctx에서 노출 */
121
+ services: GencowServicesCtx;
104
122
  /** AI 헬퍼 */
105
123
  ai?: AIContext;
124
+ /** Full-text / hybrid search helper */
125
+ search: (table: string, query: string, options: SearchOptions) => Promise<SearchResponse>;
126
+ /** Vector / semantic search helper */
127
+ vectorSearch: (table: string, options: VectorSearchOptions) => Promise<SearchResponse>;
128
+ /** Hybrid search helper (lexical + vector) */
129
+ hybridSearch: (table: string, query: string, options: HybridSearchOptions) => Promise<SearchResponse>;
130
+ /** Grounded answer helper over canonical rag_* tables */
131
+ grounding?: GroundingRuntime;
106
132
  }
107
133
  type QueryHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
108
134
  type MutationHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
@@ -175,6 +201,8 @@ declare global {
175
201
  var __gencow_connectedClients: Set<WSContext>;
176
202
  var __gencow_httpActionRegistry: HttpActionDef[];
177
203
  }
204
+ export declare function buildQuerySubscriptionKey(queryKey: string, args?: unknown): string;
205
+ export declare function subscriptionKeyMatchesQueryKey(subscriptionKey: string, queryKey: string): boolean;
178
206
  export declare function query<TSchema = any, TReturn = any>(key: string, handlerOrDef: QueryHandler<InferArgs<TSchema>, TReturn> | {
179
207
  args?: TSchema;
180
208
  public?: boolean;
@@ -272,11 +300,7 @@ export declare function deregisterClient(ws: WSContext): void;
272
300
  * @param options.buildCtxForRefresh refresh 시 query handler에 전달할 ctx 생성 함수.
273
301
  */
274
302
  export declare function buildRealtimeCtx(options?: {
275
- httpCallback?: (event: {
276
- type: "emit";
277
- queryKey: string;
278
- data: unknown;
279
- }) => void;
303
+ httpCallback?: (event: RealtimeNotifyEvent) => void;
280
304
  queryMap?: Map<string, QueryDef<any, any>>;
281
305
  buildCtxForRefresh?: () => GencowCtx;
282
306
  }): RealtimeCtx & {