@cartanova/qgrid-cli 1.7.3 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +72 -39
  2. package/bundle/dist/application/qgrid/qgrid.frame.js +12 -25
  3. package/bundle/dist/application/qgrid/qgrid.types.js +17 -4
  4. package/bundle/dist/application/qgrid/token-subscriber.js +149 -0
  5. package/bundle/dist/application/qgrid/token-trigger-setup.js +69 -0
  6. package/bundle/dist/application/sonamu.generated.sso.js +1 -1
  7. package/bundle/dist/application/token/token.types.js +1 -1
  8. package/bundle/dist/i18n/sd.generated.js +2 -2
  9. package/bundle/dist/migrations/20260410171405_create__request_logs.js +1 -1
  10. package/bundle/dist/migrations/20260410171406_create__tokens.js +1 -1
  11. package/bundle/dist/migrations/20260411120624_alter_tokens_alter2.js +1 -1
  12. package/bundle/dist/migrations/20260415134649_alter_request_logs_alter1.js +1 -1
  13. package/bundle/dist/migrations/20260421223627_alter_request_logs_add1_alter1.js +1 -1
  14. package/bundle/dist/migrations/20260423180818_alter_request_logs_add1_alter1.js +1 -1
  15. package/bundle/dist/migrations/20260423182017_alter_request_logs_add2_drop1_alter1.js +1 -1
  16. package/bundle/dist/migrations/20260423183613_alter_request_logs_add1_alter1.js +1 -1
  17. package/bundle/dist/sonamu.config.js +22 -36
  18. package/bundle/dist/testing/setup-mocks.js +1 -1
  19. package/bundle/dist/utils/subset-loaders.js +1 -1
  20. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +103 -46
  21. package/bundle/src/application/qgrid/qgrid.frame.ts +14 -26
  22. package/bundle/src/application/qgrid/qgrid.types.ts +13 -0
  23. package/bundle/src/application/qgrid/token-subscriber.ts +176 -0
  24. package/bundle/src/application/qgrid/token-trigger-setup.ts +80 -0
  25. package/bundle/src/application/request-log/request-log.entity.json +3 -1
  26. package/bundle/src/application/sonamu.generated.http +2 -1
  27. package/bundle/src/application/sonamu.generated.sso.ts +5 -5
  28. package/bundle/src/application/token/token.types.ts +1 -1
  29. package/bundle/src/i18n/sd.generated.ts +1 -1
  30. package/bundle/src/migrations/20260410171405_create__request_logs.ts +1 -1
  31. package/bundle/src/migrations/20260410171406_create__tokens.ts +1 -1
  32. package/bundle/src/migrations/20260411120624_alter_tokens_alter2.ts +1 -1
  33. package/bundle/src/migrations/20260415134649_alter_request_logs_alter1.ts +1 -1
  34. package/bundle/src/migrations/20260421223627_alter_request_logs_add1_alter1.ts +1 -1
  35. package/bundle/src/migrations/20260423180818_alter_request_logs_add1_alter1.ts +1 -1
  36. package/bundle/src/migrations/20260423182017_alter_request_logs_add2_drop1_alter1.ts +1 -1
  37. package/bundle/src/migrations/20260423183613_alter_request_logs_add1_alter1.ts +1 -1
  38. package/bundle/src/sonamu.config.ts +33 -39
  39. package/bundle/src/testing/setup-mocks.ts +1 -1
  40. package/bundle/src/typings/fastify.d.ts +1 -1
  41. package/bundle/src/typings/sonamu.d.ts +3 -2
  42. package/bundle/src/utils/subset-loaders.ts +1 -1
  43. package/bundle/web-dist/client/assets/{index-DUmwX7rR.js → index-DxJOOfGV.js} +3 -3
  44. package/bundle/web-dist/client/assets/{logs-ci_3vzTT.js → logs-D83MPvLO.js} +1 -1
  45. package/bundle/web-dist/client/assets/{routes-Ch5LFN0K.js → routes-C1PeZiBp.js} +1 -1
  46. package/bundle/web-dist/client/assets/{sd.generated-CIOZewtL.js → sd.generated-C9WnMJm4.js} +1 -1
  47. package/bundle/web-dist/client/assets/{services.generated-BrB-RELO.js → services.generated-BV5C1PQa.js} +1 -1
  48. package/bundle/web-dist/client/assets/{show-D3N15Krg.js → show-WxeogUIk.js} +1 -1
  49. package/bundle/web-dist/client/assets/tokens--RfCPd1y.js +1 -0
  50. package/bundle/web-dist/client/index.html +2 -2
  51. package/bundle/web-dist/server/assets/{copy-CKCGxwII.js → copy-SIUvcEcZ.js} +1 -1
  52. package/bundle/web-dist/server/assets/{lazyRouteComponent-BlvOfJIF.js → lazyRouteComponent-CcyhCAoL.js} +2 -2
  53. package/bundle/web-dist/server/assets/{logs-CDacVwTQ.js → logs-9j6ES80h.js} +3 -3
  54. package/bundle/web-dist/server/assets/{routes-BLm-zF86.js → routes-DQh4d03H.js} +2 -2
  55. package/bundle/web-dist/server/assets/{sd.generated-CIg_jf8e.js → sd.generated-CkiEsOwK.js} +1 -1
  56. package/bundle/web-dist/server/assets/{services.generated-wCfUuY7Z.js → services.generated-Q2ZIxMdn.js} +5 -4
  57. package/bundle/web-dist/server/assets/{shim-Dq_Izqnc.js → shim-BYpcQGN1.js} +1 -1
  58. package/bundle/web-dist/server/assets/{show-DjJyZunM.js → show-j1j13Hq1.js} +4 -4
  59. package/bundle/web-dist/server/assets/{tokens-rQIIgqMo.js → tokens-hSHCLF_7.js} +24 -5
  60. package/bundle/web-dist/server/entry-server.generated.js +7 -7
  61. package/package.json +1 -1
  62. package/bundle/web-dist/client/assets/tokens-BAlIaa6X.js +0 -1
@@ -0,0 +1,176 @@
1
+ /** tokens_changed LISTEN subscriber. */
2
+ import { getLogger } from "@logtape/logtape";
3
+ import { Client, type ClientConfig } from "pg";
4
+
5
+ import { TokenModel } from "../token/token.model";
6
+ import { type QgridDispatcherClass } from "./qgrid.dispatcher";
7
+ import { type SubscriberStatus } from "./qgrid.types";
8
+
9
+ const logger = getLogger(["qgrid", "subscriber"]);
10
+
11
+ const TOKENS_CHANGED = "tokens_changed";
12
+ const RECONCILE_INTERVAL_MS = 10 * 60 * 1000;
13
+ const CONNECTION_TIMEOUT_MS = 5_000;
14
+ const BACKOFF_BASE_MS = 500;
15
+ const BACKOFF_CAP_MS = 30_000;
16
+
17
+ type Payload = { op: "INSERT" | "UPDATE" | "DELETE"; id: number };
18
+
19
+ export class TokenSubscriber {
20
+ client: Client | null = null;
21
+ reconcileTimer: NodeJS.Timeout | null = null;
22
+ reconnectTimer: NodeJS.Timeout | null = null;
23
+ shutdownRequested = false;
24
+ attempt = 0;
25
+ connectedAt: Date | null = null;
26
+ lastReconcileAt: Date | null = null;
27
+
28
+ constructor(
29
+ public connConfig: ClientConfig,
30
+ public dispatcher: QgridDispatcherClass,
31
+ ) {}
32
+
33
+ async start(): Promise<boolean> {
34
+ this.shutdownRequested = false;
35
+ this.ensureReconcileTimer();
36
+
37
+ try {
38
+ await this.connectAndReconcile();
39
+ return true;
40
+ } catch (e) {
41
+ logger.warn(`subscriber start failed: ${(e as Error).message}, scheduling retry`);
42
+ this.retryLater();
43
+ return false;
44
+ }
45
+ }
46
+
47
+ async stop(): Promise<void> {
48
+ this.shutdownRequested = true;
49
+ if (this.reconcileTimer) {
50
+ clearInterval(this.reconcileTimer);
51
+ this.reconcileTimer = null;
52
+ }
53
+ if (this.reconnectTimer) {
54
+ clearTimeout(this.reconnectTimer);
55
+ this.reconnectTimer = null;
56
+ }
57
+ await this.closeClient();
58
+ }
59
+
60
+ status(): SubscriberStatus {
61
+ return {
62
+ connected: this.client !== null && this.reconnectTimer === null,
63
+ connectedAt: this.connectedAt,
64
+ lastReconcileAt: this.lastReconcileAt,
65
+ attempt: this.attempt,
66
+ };
67
+ }
68
+
69
+ ensureReconcileTimer(): void {
70
+ if (this.reconcileTimer) return;
71
+
72
+ // LISTEN/NOTIFY 는 끊긴 동안 유실될 수 있어 주기적으로 DB 기준으로 맞춘다.
73
+ this.reconcileTimer = setInterval(() => {
74
+ this.reconcile().catch((e) =>
75
+ logger.warn(`periodic reconcile failed: ${(e as Error).message}`),
76
+ );
77
+ }, RECONCILE_INTERVAL_MS);
78
+ }
79
+
80
+ async connectAndReconcile(): Promise<void> {
81
+ await this.connectAndListen();
82
+ await this.reconcile();
83
+ this.attempt = 0;
84
+ }
85
+
86
+ async connectAndListen(): Promise<void> {
87
+ const client = new Client({
88
+ ...this.connConfig,
89
+ connectionTimeoutMillis: CONNECTION_TIMEOUT_MS,
90
+ keepAlive: true,
91
+ keepAliveInitialDelayMillis: 10_000,
92
+ application_name: "qgrid-listener",
93
+ });
94
+
95
+ client.on("error", (e) => {
96
+ logger.warn(`subscriber error: ${e.message}`);
97
+ this.retryLater();
98
+ });
99
+ client.on("end", () => {
100
+ if (!this.shutdownRequested) this.retryLater();
101
+ });
102
+ client.on("notification", (msg) => {
103
+ if (msg.channel !== TOKENS_CHANGED || !msg.payload) return;
104
+ this.handleNotification(msg.payload).catch((e) =>
105
+ logger.warn(`handle NOTIFY failed: ${(e as Error).message}`),
106
+ );
107
+ });
108
+
109
+ try {
110
+ await client.connect();
111
+ await client.query("SET statement_timeout = 0");
112
+ await client.query("SET idle_in_transaction_session_timeout = 0");
113
+ await client.query(`LISTEN ${client.escapeIdentifier(TOKENS_CHANGED)}`);
114
+ } catch (e) {
115
+ client.removeAllListeners();
116
+ await client.end().catch(() => {});
117
+ throw e;
118
+ }
119
+
120
+ this.client = client;
121
+ this.connectedAt = new Date();
122
+ logger.info(`subscribed to ${TOKENS_CHANGED}`);
123
+ }
124
+
125
+ retryLater(): void {
126
+ if (this.shutdownRequested) return;
127
+ if (this.reconnectTimer) return;
128
+
129
+ void this.closeClient();
130
+ this.attempt += 1;
131
+ const cap = Math.min(BACKOFF_BASE_MS * 2 ** this.attempt, BACKOFF_CAP_MS);
132
+ const delay = Math.floor(Math.random() * cap);
133
+ logger.info(`reconnecting in ${delay}ms (attempt ${this.attempt})`);
134
+ this.reconnectTimer = setTimeout(() => {
135
+ this.reconnectTimer = null;
136
+ this.connectAndReconcile().catch((e) => {
137
+ logger.warn(`subscriber reconnect failed: ${(e as Error).message}`);
138
+ this.retryLater();
139
+ });
140
+ }, delay);
141
+ }
142
+
143
+ async closeClient(): Promise<void> {
144
+ const client = this.client;
145
+ if (!client) return;
146
+
147
+ this.client = null;
148
+ client.removeAllListeners();
149
+ await client.end().catch(() => {});
150
+ }
151
+
152
+ async handleNotification(payloadJson: string): Promise<void> {
153
+ const payload = JSON.parse(payloadJson) as Payload;
154
+ if (payload.op === "DELETE") {
155
+ this.dispatcher.removeCache(payload.id);
156
+ logger.info(`NOTIFY ${payload.op} id=${payload.id} → removed from cache`);
157
+ return;
158
+ }
159
+ const row = await TokenModel.findOne("A", { id: payload.id });
160
+ if (!row || !row.active) {
161
+ this.dispatcher.removeCache(payload.id);
162
+ logger.info(
163
+ `NOTIFY ${payload.op} id=${payload.id} (${row?.name ?? "?"}) active=${row?.active ?? "missing"} → removed from cache`,
164
+ );
165
+ return;
166
+ }
167
+ this.dispatcher.upsertCache(payload.id, row);
168
+ logger.info(`NOTIFY ${payload.op} id=${payload.id} (${row.name}) active=true → cache upserted`);
169
+ }
170
+
171
+ async reconcile(): Promise<void> {
172
+ const rows = await TokenModel.findActive("A");
173
+ this.dispatcher.replaceCache(rows);
174
+ this.lastReconcileAt = new Date();
175
+ }
176
+ }
@@ -0,0 +1,80 @@
1
+ /** One-time setup for public.tokens -> tokens_changed NOTIFY triggers. */
2
+ import { getLogger } from "@logtape/logtape";
3
+ import { Client, type ClientConfig } from "pg";
4
+
5
+ const logger = getLogger(["qgrid", "trigger-setup"]);
6
+
7
+ const CONNECTION_TIMEOUT_MS = 5_000;
8
+ const SETUP_LOCK_TIMEOUT_MS = 3_000;
9
+ const SETUP_STATEMENT_TIMEOUT_MS = 10_000;
10
+
11
+ const ADVISORY_LOCK_CLASS_ID = 717;
12
+ const ADVISORY_LOCK_OBJECT_ID = 44900;
13
+
14
+ const SETUP_SQL = `
15
+ CREATE OR REPLACE FUNCTION public.tokens_notify() RETURNS trigger
16
+ LANGUAGE plpgsql
17
+ SET search_path = pg_catalog, public
18
+ AS $func$
19
+ BEGIN
20
+ PERFORM pg_notify('tokens_changed', json_build_object(
21
+ 'op', TG_OP,
22
+ 'id', COALESCE(NEW.id, OLD.id)
23
+ )::text);
24
+ RETURN COALESCE(NEW, OLD);
25
+ END;
26
+ $func$;
27
+
28
+ DROP TRIGGER IF EXISTS tokens_changed_trigger ON public.tokens;
29
+ DROP TRIGGER IF EXISTS tokens_changed ON public.tokens;
30
+
31
+ CREATE OR REPLACE TRIGGER tokens_changed_ins_del
32
+ AFTER INSERT OR DELETE ON public.tokens
33
+ FOR EACH ROW EXECUTE FUNCTION public.tokens_notify();
34
+
35
+ CREATE OR REPLACE TRIGGER tokens_changed_upd
36
+ AFTER UPDATE ON public.tokens
37
+ FOR EACH ROW
38
+ WHEN (
39
+ OLD.active IS DISTINCT FROM NEW.active OR
40
+ OLD.token IS DISTINCT FROM NEW.token OR
41
+ OLD.refresh_token IS DISTINCT FROM NEW.refresh_token OR
42
+ OLD.expires_at IS DISTINCT FROM NEW.expires_at OR
43
+ OLD.name IS DISTINCT FROM NEW.name
44
+ )
45
+ EXECUTE FUNCTION public.tokens_notify();
46
+ `;
47
+
48
+ export async function ensureTokensTrigger(connConfig: ClientConfig): Promise<void> {
49
+ const client = new Client({
50
+ ...connConfig,
51
+ connectionTimeoutMillis: CONNECTION_TIMEOUT_MS,
52
+ application_name: "qgrid-trigger-setup",
53
+ });
54
+
55
+ let lockAcquired = false;
56
+ try {
57
+ await client.connect();
58
+ await client.query("SET search_path TO public");
59
+ await client.query(`SET lock_timeout = '${SETUP_LOCK_TIMEOUT_MS}ms'`);
60
+ await client.query(`SET statement_timeout = '${SETUP_STATEMENT_TIMEOUT_MS}ms'`);
61
+ await client.query("SELECT pg_advisory_lock($1, $2)", [
62
+ ADVISORY_LOCK_CLASS_ID,
63
+ ADVISORY_LOCK_OBJECT_ID,
64
+ ]);
65
+ lockAcquired = true;
66
+
67
+ await client.query(SETUP_SQL);
68
+ logger.info("trigger ensured (public.tokens_notify + tokens_changed_* triggers)");
69
+ } finally {
70
+ if (lockAcquired) {
71
+ await client
72
+ .query("SELECT pg_advisory_unlock($1, $2)", [
73
+ ADVISORY_LOCK_CLASS_ID,
74
+ ADVISORY_LOCK_OBJECT_ID,
75
+ ])
76
+ .catch(() => {});
77
+ }
78
+ await client.end().catch(() => {});
79
+ }
80
+ }
@@ -97,7 +97,9 @@
97
97
  "duration_ms",
98
98
  "cost_usd"
99
99
  ],
100
- "cone": { "note": "리스트 조회용 Preview 서브셋. TEXT 컬럼(user_prompt, system_prompt, response) 제외" }
100
+ "cone": {
101
+ "note": "리스트 조회용 Preview 서브셋. TEXT 컬럼(user_prompt, system_prompt, response) 제외"
102
+ }
101
103
  }
102
104
  },
103
105
  "enums": {
@@ -138,7 +138,8 @@ Content-Type: application/json
138
138
  "timeout": 0,
139
139
  "model": "MODEL",
140
140
  "projectName": "PROJECTNAME",
141
- "jsonSchema": "JSONSCHEMA"
141
+ "jsonSchema": "JSONSCHEMA",
142
+ "effort": "low"
142
143
  }
143
144
 
144
145
  ###
@@ -3,13 +3,13 @@
3
3
  * 직접 수정하지 마세요.
4
4
  */
5
5
 
6
- import { PuriWrapper, DatabaseSchemaExtend, PuriLoaderQueries } from "sonamu";
6
+ import { type PuriWrapper, type DatabaseSchemaExtend, type PuriLoaderQueries } from "sonamu";
7
7
 
8
8
  import {
9
- RequestLogSubsetKey,
10
- TokenSubsetKey,
11
- RequestLogBaseSchema,
12
- TokenBaseSchema,
9
+ type RequestLogSubsetKey,
10
+ type TokenSubsetKey,
11
+ type RequestLogBaseSchema,
12
+ type TokenBaseSchema,
13
13
  } from "./sonamu.generated";
14
14
 
15
15
  // SubsetQuery: RequestLog
@@ -1,4 +1,4 @@
1
- import type { z } from "zod";
1
+ import { type z } from "zod";
2
2
 
3
3
  import { TokenBaseListParams, TokenBaseSchema } from "../sonamu.generated";
4
4
 
@@ -11,7 +11,7 @@ function getCurrentLocale(): (typeof SUPPORTED_LOCALES)[number] {
11
11
  try {
12
12
  const ctx = Sonamu.getContext();
13
13
  return (ctx.locale as (typeof SUPPORTED_LOCALES)[number]) ?? DEFAULT_LOCALE;
14
- } catch (_) {
14
+ } catch {
15
15
  return DEFAULT_LOCALE;
16
16
  }
17
17
  }
@@ -1,4 +1,4 @@
1
- import type { Knex } from "knex";
1
+ import { type Knex } from "knex";
2
2
 
3
3
  export async function up(knex: Knex): Promise<void> {
4
4
  await knex.schema.createTable("request_logs", (table) => {
@@ -1,4 +1,4 @@
1
- import type { Knex } from "knex";
1
+ import { type Knex } from "knex";
2
2
 
3
3
  export async function up(knex: Knex): Promise<void> {
4
4
  await knex.schema.createTable("tokens", (table) => {
@@ -1,4 +1,4 @@
1
- import type { Knex } from "knex";
1
+ import { type Knex } from "knex";
2
2
 
3
3
  export async function up(knex: Knex): Promise<void> {
4
4
  await knex.schema.alterTable("tokens", (table) => {
@@ -1,4 +1,4 @@
1
- import { Knex } from "knex";
1
+ import { type Knex } from "knex";
2
2
 
3
3
  export async function up(knex: Knex): Promise<void> {
4
4
  await knex.raw(
@@ -1,4 +1,4 @@
1
- import { Knex } from "knex";
1
+ import { type Knex } from "knex";
2
2
 
3
3
  export async function up(knex: Knex): Promise<void> {
4
4
  await knex.schema.alterTable("request_logs", (table) => {
@@ -1,4 +1,4 @@
1
- import { Knex } from "knex";
1
+ import { type Knex } from "knex";
2
2
 
3
3
  export async function up(knex: Knex): Promise<void> {
4
4
  await knex.schema.alterTable("request_logs", (table) => {
@@ -1,4 +1,4 @@
1
- import { Knex } from "knex";
1
+ import { type Knex } from "knex";
2
2
 
3
3
  export async function up(knex: Knex): Promise<void> {
4
4
  await knex.schema.alterTable("request_logs", (table) => {
@@ -1,4 +1,4 @@
1
- import { Knex } from "knex";
1
+ import { type Knex } from "knex";
2
2
 
3
3
  export async function up(knex: Knex): Promise<void> {
4
4
  await knex.schema.alterTable("request_logs", (table) => {
@@ -5,23 +5,27 @@ import { getPrettyFormatter } from "@logtape/pretty";
5
5
  import dotenv from "dotenv";
6
6
  import { CachePresets, defineConfig } from "sonamu";
7
7
  import { drivers as cacheDrivers, store } from "sonamu/cache";
8
- import { drivers } from "sonamu/storage";
9
8
 
10
9
  dotenv.config({ path: path.join(import.meta.dirname, "../.env") });
11
10
 
12
11
  const host = process.env.HOST ?? "localhost";
13
12
  const port = Number(process.env.PORT ?? 44900);
14
13
 
14
+ const connConfig = {
15
+ host: process.env.QGRID_DB_HOST ?? "localhost",
16
+ port: Number(process.env.QGRID_DB_PORT ?? 44901),
17
+ user: process.env.QGRID_DB_USER ?? "postgres",
18
+ password: process.env.QGRID_DB_PASSWORD ?? "postgres",
19
+ database: process.env.QGRID_DB_NAME ?? "qgrid",
20
+ };
21
+
15
22
  export default defineConfig({
16
23
  projectName: process.env.PROJECT_NAME ?? "SonamuProject",
17
24
  database: {
18
25
  name: process.env.QGRID_DB_NAME ?? "qgrid",
19
26
  defaultOptions: {
20
27
  connection: {
21
- host: process.env.QGRID_DB_HOST ?? "localhost",
22
- port: Number(process.env.QGRID_DB_PORT ?? 44901),
23
- user: process.env.QGRID_DB_USER ?? "postgres",
24
- password: process.env.QGRID_DB_PASSWORD ?? "postgres",
28
+ ...connConfig,
25
29
  keepAlive: true,
26
30
  keepAliveInitialDelayMillis: 10000,
27
31
  },
@@ -164,31 +168,6 @@ export default defineConfig({
164
168
  }
165
169
  },
166
170
  },
167
- storage: {
168
- drivers: {
169
- fs: drivers.fs({
170
- location: path.join(import.meta.dirname, "/../public/uploaded"),
171
- visibility: "public",
172
- urlBuilder: {
173
- async generateURL(key) {
174
- return `/api/public/uploaded/${key}`;
175
- },
176
- async generateSignedURL(key) {
177
- return `/api/public/uploaded/${key}`;
178
- },
179
- },
180
- }),
181
- s3: drivers.s3({
182
- credentials: {
183
- accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
184
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
185
- },
186
- region: process.env.S3_REGION ?? "ap-northeast-2",
187
- bucket: process.env.S3_BUCKET ?? "sonamu_default_bucket",
188
- visibility: "private",
189
- }),
190
- },
191
- },
192
171
  cache: {
193
172
  default: "main",
194
173
  stores: {
@@ -216,19 +195,34 @@ export default defineConfig({
216
195
  }
217
196
 
218
197
  const { QgridDispatcher } = await import("./application/qgrid/qgrid.dispatcher");
198
+ const { ensureTokensTrigger } = await import("./application/qgrid/token-trigger-setup");
199
+ const { TokenSubscriber } = await import("./application/qgrid/token-subscriber");
200
+
201
+ let triggerReady = true;
219
202
  try {
220
- const { TokenModel } = await import("./application/token/token.model");
221
- const entries = await TokenModel.findActive("A");
222
- for (const e of entries) QgridDispatcher.addToken(e.token, e.name);
223
- console.log(
224
- `🌲 Server listening on http://${host}:${port} (${QgridDispatcher.tokens.size} tokens loaded)`,
225
- );
203
+ await ensureTokensTrigger(connConfig);
226
204
  } catch (e) {
227
- console.warn(`⚠ Dispatcher init failed: ${(e as Error).message}`);
228
- console.log(`🌲 Server listening on http://${host}:${port} (no tokens)`);
205
+ triggerReady = false;
206
+ console.warn(
207
+ `⚠ Token trigger setup failed: ${(e as Error).message}. Continuing with LISTEN/reconcile.`,
208
+ );
229
209
  }
210
+
211
+ const subscriber = new TokenSubscriber(connConfig, QgridDispatcher);
212
+ QgridDispatcher.subscriber = subscriber;
213
+
214
+ const started = await subscriber.start();
215
+ const tokenInfo = started
216
+ ? `${QgridDispatcher.tokens.size} active tokens, LISTEN active`
217
+ : `degraded — token sync retry scheduled`;
218
+ const triggerInfo = triggerReady ? "" : ", trigger setup failed";
219
+ console.log(`🌲 Server listening on http://${host}:${port} (${tokenInfo}${triggerInfo})`);
230
220
  },
231
- onShutdown: () => {
221
+ onShutdown: async () => {
222
+ const { QgridDispatcher } = await import("./application/qgrid/qgrid.dispatcher");
223
+ if (QgridDispatcher.subscriber) {
224
+ await QgridDispatcher.subscriber.stop();
225
+ }
232
226
  console.log("graceful shutdown");
233
227
  },
234
228
  onError: (error, _request, reply) => {
@@ -1,4 +1,4 @@
1
- import type { PathLike } from "fs";
1
+ import { type PathLike } from "fs";
2
2
 
3
3
  // import { Naite } from "sonamu";
4
4
  import { vi } from "vitest";
@@ -1,7 +1,7 @@
1
1
  /** biome-ignore-all lint/correctness/noUnusedImports: d.ts */
2
2
  import type fastify from "fastify";
3
3
 
4
- import type { UserSubsetSS } from "../application/sonamu.generated";
4
+ import { type UserSubsetSS } from "../application/sonamu.generated";
5
5
 
6
6
  declare module "fastify" {
7
7
  export interface PassportUser extends UserSubsetSS {}
@@ -1,6 +1,5 @@
1
1
  /** biome-ignore-all lint/correctness/noUnusedImports: d.ts */
2
-
3
- import {} from "sonamu";
2
+ /* oxlint-disable no-empty-imports */
4
3
 
5
4
  declare module "sonamu" {
6
5
  export interface ContextExtend {
@@ -15,3 +14,5 @@ declare module "sonamu" {
15
14
  // CustomGuardKey: true
16
15
  }
17
16
  }
17
+
18
+ export {};
@@ -1,4 +1,4 @@
1
- import type { SubsetQuery } from "sonamu";
1
+ import { type SubsetQuery } from "sonamu";
2
2
 
3
3
  export function getSubsetLoaders(subsets: string[], subsetQueries: Record<string, SubsetQuery>) {
4
4
  return subsets.reduce(