@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
|
@@ -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
|
+
}
|
package/dist/reactive.d.ts
CHANGED
|
@@ -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 & {
|