@syncular/server-hono 0.0.6-96 → 0.2.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 (104) hide show
  1. package/README.md +5 -23
  2. package/dist/admin-page.d.ts +14 -0
  3. package/dist/admin-page.js +217 -0
  4. package/dist/admin.d.ts +39 -0
  5. package/dist/admin.js +142 -0
  6. package/dist/index.d.ts +16 -14
  7. package/dist/index.js +134 -23
  8. package/package.json +19 -53
  9. package/src/admin-page.ts +217 -0
  10. package/src/admin.ts +193 -0
  11. package/src/index.ts +175 -31
  12. package/dist/api-key-auth.d.ts +0 -49
  13. package/dist/api-key-auth.d.ts.map +0 -1
  14. package/dist/api-key-auth.js +0 -110
  15. package/dist/api-key-auth.js.map +0 -1
  16. package/dist/blobs.d.ts +0 -69
  17. package/dist/blobs.d.ts.map +0 -1
  18. package/dist/blobs.js +0 -383
  19. package/dist/blobs.js.map +0 -1
  20. package/dist/console/gateway.d.ts +0 -33
  21. package/dist/console/gateway.d.ts.map +0 -1
  22. package/dist/console/gateway.js +0 -2534
  23. package/dist/console/gateway.js.map +0 -1
  24. package/dist/console/index.d.ts +0 -11
  25. package/dist/console/index.d.ts.map +0 -1
  26. package/dist/console/index.js +0 -11
  27. package/dist/console/index.js.map +0 -1
  28. package/dist/console/routes.d.ts +0 -33
  29. package/dist/console/routes.d.ts.map +0 -1
  30. package/dist/console/routes.js +0 -3029
  31. package/dist/console/routes.js.map +0 -1
  32. package/dist/console/schemas.d.ts +0 -490
  33. package/dist/console/schemas.d.ts.map +0 -1
  34. package/dist/console/schemas.js +0 -303
  35. package/dist/console/schemas.js.map +0 -1
  36. package/dist/console/types.d.ts +0 -175
  37. package/dist/console/types.d.ts.map +0 -1
  38. package/dist/console/types.js +0 -2
  39. package/dist/console/types.js.map +0 -1
  40. package/dist/console/ui.d.ts +0 -38
  41. package/dist/console/ui.d.ts.map +0 -1
  42. package/dist/console/ui.js +0 -43
  43. package/dist/console/ui.js.map +0 -1
  44. package/dist/create-server.d.ts +0 -68
  45. package/dist/create-server.d.ts.map +0 -1
  46. package/dist/create-server.js +0 -110
  47. package/dist/create-server.js.map +0 -1
  48. package/dist/index.d.ts.map +0 -1
  49. package/dist/index.js.map +0 -1
  50. package/dist/openapi.d.ts +0 -45
  51. package/dist/openapi.d.ts.map +0 -1
  52. package/dist/openapi.js +0 -59
  53. package/dist/openapi.js.map +0 -1
  54. package/dist/proxy/connection-manager.d.ts +0 -78
  55. package/dist/proxy/connection-manager.d.ts.map +0 -1
  56. package/dist/proxy/connection-manager.js +0 -251
  57. package/dist/proxy/connection-manager.js.map +0 -1
  58. package/dist/proxy/index.d.ts +0 -8
  59. package/dist/proxy/index.d.ts.map +0 -1
  60. package/dist/proxy/index.js +0 -8
  61. package/dist/proxy/index.js.map +0 -1
  62. package/dist/proxy/routes.d.ts +0 -74
  63. package/dist/proxy/routes.d.ts.map +0 -1
  64. package/dist/proxy/routes.js +0 -147
  65. package/dist/proxy/routes.js.map +0 -1
  66. package/dist/rate-limit.d.ts +0 -101
  67. package/dist/rate-limit.d.ts.map +0 -1
  68. package/dist/rate-limit.js +0 -186
  69. package/dist/rate-limit.js.map +0 -1
  70. package/dist/routes.d.ts +0 -161
  71. package/dist/routes.d.ts.map +0 -1
  72. package/dist/routes.js +0 -1493
  73. package/dist/routes.js.map +0 -1
  74. package/dist/ws.d.ts +0 -235
  75. package/dist/ws.d.ts.map +0 -1
  76. package/dist/ws.js +0 -614
  77. package/dist/ws.js.map +0 -1
  78. package/src/__tests__/blob-routes.test.ts +0 -424
  79. package/src/__tests__/console-gateway-live-routes.test.ts +0 -297
  80. package/src/__tests__/console-gateway-routes.test.ts +0 -1364
  81. package/src/__tests__/console-routes.test.ts +0 -1626
  82. package/src/__tests__/console-ui.test.ts +0 -114
  83. package/src/__tests__/create-server.test.ts +0 -485
  84. package/src/__tests__/pull-chunk-storage.test.ts +0 -576
  85. package/src/__tests__/rate-limit.test.ts +0 -78
  86. package/src/__tests__/realtime-bridge.test.ts +0 -135
  87. package/src/__tests__/sync-rate-limit-routing.test.ts +0 -185
  88. package/src/__tests__/ws-connection-manager.test.ts +0 -176
  89. package/src/api-key-auth.ts +0 -179
  90. package/src/blobs.ts +0 -534
  91. package/src/console/gateway.ts +0 -3603
  92. package/src/console/index.ts +0 -11
  93. package/src/console/routes.ts +0 -3936
  94. package/src/console/schemas.ts +0 -434
  95. package/src/console/types.ts +0 -185
  96. package/src/console/ui.ts +0 -100
  97. package/src/create-server.ts +0 -207
  98. package/src/openapi.ts +0 -74
  99. package/src/proxy/connection-manager.ts +0 -340
  100. package/src/proxy/index.ts +0 -8
  101. package/src/proxy/routes.ts +0 -223
  102. package/src/rate-limit.ts +0 -321
  103. package/src/routes.ts +0 -2059
  104. package/src/ws.ts +0 -802
package/src/console/ui.ts DELETED
@@ -1,100 +0,0 @@
1
- import {
2
- type ConsoleStaticResponder,
3
- type ConsoleUiPrefill,
4
- type CreateConsoleStaticResponderOptions,
5
- createConsoleStaticResponder,
6
- type ServeConsoleStaticRequestOptions,
7
- } from '@syncular/console/server';
8
- import type { Context, Hono } from 'hono';
9
-
10
- type MaybePromise<T> = T | Promise<T>;
11
-
12
- export interface MountConsoleUiOptions {
13
- /**
14
- * Browser mount path for the console UI. Defaults to `/console`.
15
- */
16
- mountPath?: string;
17
- /**
18
- * API base path used to prefill the console server URL.
19
- * Defaults to `/api`.
20
- */
21
- apiBasePath?: string;
22
- /**
23
- * Resolve prefill values per request.
24
- * Return `null`/`undefined` to keep defaults.
25
- */
26
- resolvePrefill?: (
27
- c: Context
28
- ) => MaybePromise<ConsoleUiPrefill | null | undefined>;
29
- /**
30
- * Convenience callback for prefilled token.
31
- */
32
- resolveToken?: (c: Context) => MaybePromise<string | undefined>;
33
- /**
34
- * Override distributable directory.
35
- */
36
- staticDir?: string;
37
- /**
38
- * Cache-Control header for index responses.
39
- */
40
- indexCacheControl?: string;
41
- /**
42
- * Cache-Control header for static asset responses.
43
- */
44
- assetCacheControl?: string;
45
- }
46
-
47
- type StaticResponderConfig = Pick<
48
- CreateConsoleStaticResponderOptions,
49
- 'mountPath' | 'staticDir' | 'indexCacheControl' | 'assetCacheControl'
50
- >;
51
-
52
- function normalizePath(pathname: string | undefined, fallback: string): string {
53
- const value = pathname?.trim() ?? '';
54
- if (!value) return fallback;
55
- if (value === '/') return '/';
56
- const withLeadingSlash = value.startsWith('/') ? value : `/${value}`;
57
- return withLeadingSlash.replace(/\/+$/g, '') || '/';
58
- }
59
-
60
- export function mountConsoleUi(
61
- app: Hono,
62
- options: MountConsoleUiOptions = {}
63
- ): void {
64
- const mountPath = normalizePath(options.mountPath, '/console');
65
- const apiBasePath = normalizePath(options.apiBasePath, '/api');
66
-
67
- const staticResponderConfig: StaticResponderConfig = {
68
- mountPath,
69
- staticDir: options.staticDir,
70
- indexCacheControl: options.indexCacheControl,
71
- assetCacheControl: options.assetCacheControl,
72
- };
73
- const serveConsoleStatic: ConsoleStaticResponder =
74
- createConsoleStaticResponder(staticResponderConfig);
75
-
76
- const handler = async (c: Context) => {
77
- const prefillFromResolver = await options.resolvePrefill?.(c);
78
- const token = await options.resolveToken?.(c);
79
- const origin = new URL(c.req.url).origin;
80
-
81
- const prefill: ConsoleUiPrefill = {
82
- basePath: mountPath,
83
- serverUrl: `${origin}${apiBasePath}`,
84
- ...prefillFromResolver,
85
- };
86
-
87
- if (token !== undefined) {
88
- prefill.token = token;
89
- }
90
-
91
- const requestOptions: ServeConsoleStaticRequestOptions = { prefill };
92
- const response = await serveConsoleStatic(c.req.raw, requestOptions);
93
- if (!response) return c.notFound();
94
- return response;
95
- };
96
-
97
- const wildcardPath = mountPath === '/' ? '/*' : `${mountPath}/*`;
98
- app.get(mountPath, handler);
99
- app.get(wildcardPath, handler);
100
- }
@@ -1,207 +0,0 @@
1
- /**
2
- * Simplified server factory for Hono
3
- *
4
- * Breaking changes from legacy createSyncRoutes:
5
- * - sync contract instead of top-level handlers/authenticate
6
- * - Combined sync + console routes in one call
7
- */
8
-
9
- import type {
10
- ScopeCacheBackend,
11
- ServerSyncConfig,
12
- ServerSyncDialect,
13
- SnapshotChunkStorage,
14
- SqlFamily,
15
- SyncCoreDb,
16
- } from '@syncular/server';
17
- import type { UpgradeWebSocket } from 'hono/ws';
18
- import type { Kysely } from 'kysely';
19
- import {
20
- createConsoleEventEmitter,
21
- createConsoleRoutes,
22
- createTokenAuthenticator,
23
- } from './console/routes';
24
- import type {
25
- ConsoleEventEmitter,
26
- ConsoleSharedOptions,
27
- } from './console/types';
28
- import {
29
- createSyncRoutes,
30
- getSyncWebSocketConnectionManager,
31
- type SyncAuthResult,
32
- type SyncRoutesConfigWithRateLimit,
33
- } from './routes';
34
-
35
- export interface SyncServerOptions<
36
- DB extends SyncCoreDb = SyncCoreDb,
37
- Auth extends SyncAuthResult = SyncAuthResult,
38
- F extends SqlFamily = SqlFamily,
39
- > {
40
- /** Kysely database instance */
41
- db: Kysely<DB>;
42
-
43
- /** Server sync dialect */
44
- dialect: ServerSyncDialect<F>;
45
-
46
- /** Sync contract with auth + table handlers */
47
- sync: ServerSyncConfig<DB, Auth>;
48
-
49
- /** Snapshot chunk storage (external body storage, e.g. R2/S3) */
50
- chunkStorage?: SnapshotChunkStorage;
51
-
52
- /** Scope cache backend for resolveScopes() results */
53
- scopeCache?: ScopeCacheBackend;
54
-
55
- /** Sync route configuration */
56
- routes?: SyncRoutesConfigWithRateLimit;
57
-
58
- /** WebSocket upgrader for realtime */
59
- upgradeWebSocket?: UpgradeWebSocket;
60
-
61
- /**
62
- * Console configuration for dashboard/monitoring.
63
- * Omit or set to false to disable console routes.
64
- */
65
- console?:
66
- | false
67
- | ({
68
- /** Console bearer token for authentication (required unless SYNC_CONSOLE_TOKEN is set) */
69
- token?: string;
70
- } & ConsoleSharedOptions);
71
- }
72
-
73
- export interface SyncServerResult {
74
- /** Sync routes for Hono */
75
- syncRoutes: ReturnType<typeof createSyncRoutes>;
76
- /** Console routes for Hono (if enabled) */
77
- consoleRoutes?: ReturnType<typeof createConsoleRoutes>;
78
- /** Console event emitter (if console enabled) */
79
- consoleEventEmitter?: ConsoleEventEmitter;
80
- }
81
-
82
- /**
83
- * Create a simplified sync server with sync and optional console routes.
84
- *
85
- * @example
86
- * ```typescript
87
- * // With sync contract
88
- * const { syncRoutes } = createSyncServer({
89
- * db,
90
- * dialect,
91
- * sync,
92
- * });
93
- *
94
- * // With custom handlers
95
- * const { syncRoutes, consoleRoutes } = createSyncServer({
96
- * db,
97
- * dialect,
98
- * sync,
99
- * console: { token: process.env.CONSOLE_TOKEN },
100
- * });
101
- * ```
102
- */
103
- export function createSyncServer<
104
- DB extends SyncCoreDb = SyncCoreDb,
105
- Auth extends SyncAuthResult = SyncAuthResult,
106
- F extends SqlFamily = SqlFamily,
107
- >(options: SyncServerOptions<DB, Auth, F>): SyncServerResult {
108
- const {
109
- db,
110
- dialect,
111
- sync,
112
- chunkStorage,
113
- scopeCache,
114
- routes,
115
- upgradeWebSocket,
116
- console: consoleConfig,
117
- } = options;
118
-
119
- if (sync.handlers.length === 0) {
120
- throw new Error('At least one handler must be provided');
121
- }
122
-
123
- const resolvedConsoleConfig =
124
- consoleConfig === false || consoleConfig === undefined
125
- ? undefined
126
- : consoleConfig;
127
- const isConsoleEnabled = Boolean(resolvedConsoleConfig);
128
- const consoleToken = isConsoleEnabled
129
- ? (resolvedConsoleConfig?.token ?? process.env.SYNC_CONSOLE_TOKEN)
130
- : undefined;
131
-
132
- if (isConsoleEnabled && !consoleToken) {
133
- throw new Error(
134
- 'Console is enabled but no token is configured. Set `console.token` or SYNC_CONSOLE_TOKEN.'
135
- );
136
- }
137
-
138
- const consoleEventEmitter = isConsoleEnabled
139
- ? createConsoleEventEmitter()
140
- : undefined;
141
- const consoleSchemaReady =
142
- isConsoleEnabled && dialect.ensureConsoleSchema
143
- ? dialect.ensureConsoleSchema(db)
144
- : undefined;
145
-
146
- // Create sync routes
147
- const syncRoutes = createSyncRoutes({
148
- db,
149
- dialect,
150
- handlers: sync.handlers,
151
- authenticate: async (context): Promise<Auth | null> =>
152
- sync.authenticate(context.req.raw),
153
- chunkStorage,
154
- scopeCache,
155
- consoleLiveEmitter: consoleEventEmitter,
156
- consoleSchemaReady,
157
- sync: {
158
- ...routes,
159
- websocket: upgradeWebSocket
160
- ? {
161
- enabled: true,
162
- upgradeWebSocket,
163
- ...(routes?.websocket?.heartbeatIntervalMs !== undefined && {
164
- heartbeatIntervalMs: routes.websocket.heartbeatIntervalMs,
165
- }),
166
- ...(routes?.websocket?.maxConnectionsTotal !== undefined && {
167
- maxConnectionsTotal: routes.websocket.maxConnectionsTotal,
168
- }),
169
- ...(routes?.websocket?.maxConnectionsPerClient !== undefined && {
170
- maxConnectionsPerClient: routes.websocket.maxConnectionsPerClient,
171
- }),
172
- }
173
- : { enabled: false },
174
- },
175
- });
176
-
177
- // Console is opt-in; disable unless explicitly configured.
178
- if (!resolvedConsoleConfig) {
179
- return { syncRoutes };
180
- }
181
-
182
- const consoleRoutes = createConsoleRoutes({
183
- db,
184
- dialect,
185
- handlers: sync.handlers,
186
- authenticate: createTokenAuthenticator(consoleToken),
187
- corsOrigins: resolvedConsoleConfig.corsOrigins,
188
- eventEmitter: consoleEventEmitter,
189
- consoleSchemaReady,
190
- wsConnectionManager: getSyncWebSocketConnectionManager(syncRoutes),
191
- metrics: resolvedConsoleConfig.metrics,
192
- maintenance: resolvedConsoleConfig.maintenance,
193
- blobBucket: resolvedConsoleConfig.blobBucket,
194
- ...(upgradeWebSocket && {
195
- websocket: {
196
- enabled: true,
197
- upgradeWebSocket,
198
- },
199
- }),
200
- });
201
-
202
- return {
203
- syncRoutes,
204
- consoleRoutes,
205
- consoleEventEmitter,
206
- };
207
- }
package/src/openapi.ts DELETED
@@ -1,74 +0,0 @@
1
- /**
2
- * @syncular/server-hono - OpenAPI Spec Export
3
- *
4
- * Provides utilities for generating and serving OpenAPI specifications.
5
- */
6
-
7
- import type { Hono } from 'hono';
8
- import { generateSpecs, openAPIRouteHandler } from 'hono-openapi';
9
-
10
- interface OpenAPIConfig {
11
- title?: string;
12
- version?: string;
13
- description?: string;
14
- servers?: Array<{ url: string; description?: string }>;
15
- }
16
-
17
- /**
18
- * Create an OpenAPI spec handler that can be used with Hono routes.
19
- *
20
- * @example
21
- * ```typescript
22
- * import { Hono } from 'hono';
23
- * import { createSyncRoutes, createConsoleRoutes, createOpenAPIHandler } from '@syncular/server-hono';
24
- *
25
- * const app = new Hono();
26
- * const syncRoutes = createSyncRoutes({ ... });
27
- * const consoleRoutes = createConsoleRoutes({ ... });
28
- *
29
- * app.route('/sync', syncRoutes);
30
- * app.route('/console', consoleRoutes);
31
- *
32
- * // Add OpenAPI spec endpoint
33
- * app.get('/openapi.json', createOpenAPIHandler(app, {
34
- * title: 'Syncular API',
35
- * version: '1.0.0',
36
- * }));
37
- * ```
38
- */
39
- export function createOpenAPIHandler(app: Hono, config: OpenAPIConfig = {}) {
40
- return openAPIRouteHandler(app, {
41
- documentation: {
42
- info: {
43
- title: config.title ?? 'Syncular API',
44
- version: config.version ?? '1.0.0',
45
- description:
46
- config.description ??
47
- 'Sync infrastructure API for real-time data synchronization',
48
- },
49
- servers: config.servers,
50
- },
51
- });
52
- }
53
-
54
- /**
55
- * Generate OpenAPI document from a Hono app instance.
56
- * This is useful for build-time spec generation.
57
- */
58
- export async function generateOpenAPIDocument(
59
- app: Hono,
60
- config: OpenAPIConfig = {}
61
- ): Promise<unknown> {
62
- return generateSpecs(app, {
63
- documentation: {
64
- info: {
65
- title: config.title ?? 'Syncular API',
66
- version: config.version ?? '1.0.0',
67
- description:
68
- config.description ??
69
- 'Sync infrastructure API for real-time data synchronization',
70
- },
71
- servers: config.servers,
72
- },
73
- });
74
- }
@@ -1,340 +0,0 @@
1
- /**
2
- * @syncular/server-hono - Proxy Connection Manager
3
- *
4
- * Manages WebSocket connections for the proxy.
5
- */
6
-
7
- import type {
8
- ProxyHandshake,
9
- ProxyMessage,
10
- ProxyResponse,
11
- } from '@syncular/core';
12
- import type {
13
- ExecuteProxyQueryResult,
14
- ProxyHandlerCollection,
15
- ServerSyncDialect,
16
- SyncCoreDb,
17
- } from '@syncular/server';
18
- import { executeProxyQuery } from '@syncular/server';
19
- import type { WSContext } from 'hono/ws';
20
- import type { Kysely, Transaction } from 'kysely';
21
-
22
- export interface ProxyConnectionManagerConfig<
23
- DB extends SyncCoreDb = SyncCoreDb,
24
- > {
25
- /** Database connection */
26
- db: Kysely<DB>;
27
- /** Server sync dialect */
28
- dialect: ServerSyncDialect;
29
- /** Proxy table handlers for oplog generation */
30
- handlers: ProxyHandlerCollection;
31
- /** Maximum concurrent connections (default: 100) */
32
- maxConnections?: number;
33
- /** Idle connection timeout in ms (default: 30000) */
34
- idleTimeoutMs?: number;
35
- }
36
-
37
- interface ProxyConnectionState<DB extends SyncCoreDb> {
38
- ws: WSContext;
39
- actorId: string;
40
- clientId: string;
41
- transaction: Transaction<DB> | null;
42
- lastActivity: number;
43
- idleTimer: ReturnType<typeof setTimeout> | null;
44
- /** Transaction promise resolve callback */
45
- __resolveTransaction?: () => void;
46
- /** Transaction promise reject callback */
47
- __rejectTransaction?: (error: Error) => void;
48
- }
49
-
50
- /**
51
- * Manages proxy WebSocket connections and their state.
52
- */
53
- export class ProxyConnectionManager<DB extends SyncCoreDb = SyncCoreDb> {
54
- private connections = new Map<WSContext, ProxyConnectionState<DB>>();
55
- private config: ProxyConnectionManagerConfig<DB>;
56
- private idleTimeoutMs: number;
57
- private maxConnections: number;
58
-
59
- constructor(config: ProxyConnectionManagerConfig<DB>) {
60
- this.config = config;
61
- this.idleTimeoutMs = config.idleTimeoutMs ?? 30000;
62
- this.maxConnections = config.maxConnections ?? 100;
63
- }
64
-
65
- /**
66
- * Check if a new connection can be accepted.
67
- */
68
- canAccept(): boolean {
69
- return this.connections.size < this.maxConnections;
70
- }
71
-
72
- /**
73
- * Get the current connection count.
74
- */
75
- getConnectionCount(): number {
76
- return this.connections.size;
77
- }
78
-
79
- /**
80
- * Handle the handshake message and register the connection.
81
- */
82
- register(ws: WSContext, handshake: ProxyHandshake): ProxyConnectionState<DB> {
83
- const state: ProxyConnectionState<DB> = {
84
- ws,
85
- actorId: handshake.actorId,
86
- clientId: handshake.clientId,
87
- transaction: null,
88
- lastActivity: Date.now(),
89
- idleTimer: null,
90
- };
91
-
92
- this.connections.set(ws, state);
93
- this.resetIdleTimer(state);
94
-
95
- return state;
96
- }
97
-
98
- /**
99
- * Get the connection state for a WebSocket.
100
- */
101
- get(ws: WSContext): ProxyConnectionState<DB> | undefined {
102
- return this.connections.get(ws);
103
- }
104
-
105
- /**
106
- * Unregister and cleanup a connection.
107
- */
108
- async unregister(ws: WSContext): Promise<void> {
109
- const state = this.connections.get(ws);
110
- if (!state) return;
111
-
112
- // Clear idle timer
113
- if (state.idleTimer) {
114
- clearTimeout(state.idleTimer);
115
- }
116
-
117
- // Rollback any pending transaction by rejecting the promise
118
- if (state.transaction) {
119
- const rejectTransaction = state.__rejectTransaction;
120
- if (rejectTransaction) {
121
- rejectTransaction(new Error('Connection closed'));
122
- }
123
- state.transaction = null;
124
- state.__resolveTransaction = undefined;
125
- state.__rejectTransaction = undefined;
126
- }
127
-
128
- this.connections.delete(ws);
129
- }
130
-
131
- /**
132
- * Handle a proxy message and return the response.
133
- */
134
- async handleMessage(
135
- ws: WSContext,
136
- message: ProxyMessage
137
- ): Promise<ProxyResponse> {
138
- const state = this.connections.get(ws);
139
- if (!state) {
140
- return {
141
- id: message.id,
142
- type: 'error',
143
- error: 'Connection not registered',
144
- };
145
- }
146
-
147
- // Update activity and reset idle timer
148
- state.lastActivity = Date.now();
149
- this.resetIdleTimer(state);
150
-
151
- try {
152
- switch (message.type) {
153
- case 'begin':
154
- return await this.handleBegin(state, message);
155
-
156
- case 'commit':
157
- return await this.handleCommit(state, message);
158
-
159
- case 'rollback':
160
- return await this.handleRollback(state, message);
161
-
162
- case 'query':
163
- return await this.handleQuery(state, message);
164
-
165
- default:
166
- return {
167
- id: message.id,
168
- type: 'error',
169
- error: `Unknown message type: ${message.type}`,
170
- };
171
- }
172
- } catch (err) {
173
- return {
174
- id: message.id,
175
- type: 'error',
176
- error: err instanceof Error ? err.message : 'Unknown error',
177
- };
178
- }
179
- }
180
-
181
- private async handleBegin(
182
- state: ProxyConnectionState<DB>,
183
- message: ProxyMessage
184
- ): Promise<ProxyResponse> {
185
- if (state.transaction) {
186
- return {
187
- id: message.id,
188
- type: 'error',
189
- error: 'Transaction already in progress',
190
- };
191
- }
192
-
193
- // Start a transaction and keep it open
194
- // We use a workaround since Kysely doesn't expose raw transaction control
195
- return new Promise((resolve) => {
196
- this.config.db
197
- .transaction()
198
- .execute(async (trx) => {
199
- state.transaction = trx;
200
-
201
- // Wait for commit or rollback
202
- return new Promise<void>((resolveTransaction, rejectTransaction) => {
203
- state.__resolveTransaction = resolveTransaction;
204
- state.__rejectTransaction = rejectTransaction;
205
- resolve({
206
- id: message.id,
207
- type: 'result',
208
- });
209
- });
210
- })
211
- .catch(() => {
212
- // Transaction was rolled back externally
213
- });
214
- });
215
- }
216
-
217
- private async handleCommit(
218
- state: ProxyConnectionState<DB>,
219
- message: ProxyMessage
220
- ): Promise<ProxyResponse> {
221
- if (!state.transaction) {
222
- return {
223
- id: message.id,
224
- type: 'error',
225
- error: 'No transaction in progress',
226
- };
227
- }
228
-
229
- // Resolve the transaction promise to commit
230
- const resolveTransaction = state.__resolveTransaction;
231
- if (resolveTransaction) {
232
- resolveTransaction();
233
- }
234
- state.transaction = null;
235
- state.__resolveTransaction = undefined;
236
- state.__rejectTransaction = undefined;
237
-
238
- return {
239
- id: message.id,
240
- type: 'result',
241
- };
242
- }
243
-
244
- private async handleRollback(
245
- state: ProxyConnectionState<DB>,
246
- message: ProxyMessage
247
- ): Promise<ProxyResponse> {
248
- if (!state.transaction) {
249
- return {
250
- id: message.id,
251
- type: 'error',
252
- error: 'No transaction in progress',
253
- };
254
- }
255
-
256
- // Reject the transaction promise to trigger rollback
257
- const rejectTransaction = state.__rejectTransaction;
258
- if (rejectTransaction) {
259
- rejectTransaction(new Error('Transaction rolled back'));
260
- }
261
- state.transaction = null;
262
- state.__resolveTransaction = undefined;
263
- state.__rejectTransaction = undefined;
264
-
265
- return {
266
- id: message.id,
267
- type: 'result',
268
- };
269
- }
270
-
271
- private async handleQuery(
272
- state: ProxyConnectionState<DB>,
273
- message: ProxyMessage
274
- ): Promise<ProxyResponse> {
275
- if (!message.sql) {
276
- return {
277
- id: message.id,
278
- type: 'error',
279
- error: 'Missing SQL query',
280
- };
281
- }
282
-
283
- const db = state.transaction ?? this.config.db;
284
-
285
- const result: ExecuteProxyQueryResult = await executeProxyQuery({
286
- db,
287
- dialect: this.config.dialect,
288
- handlers: this.config.handlers,
289
- ctx: {
290
- actorId: state.actorId,
291
- clientId: state.clientId,
292
- },
293
- sqlQuery: message.sql,
294
- parameters: message.parameters ?? [],
295
- });
296
-
297
- return {
298
- id: message.id,
299
- type: 'result',
300
- rows: result.rows,
301
- rowCount: result.rowCount,
302
- };
303
- }
304
-
305
- private resetIdleTimer(state: ProxyConnectionState<DB>): void {
306
- if (state.idleTimer) {
307
- clearTimeout(state.idleTimer);
308
- }
309
-
310
- if (this.idleTimeoutMs <= 0) return;
311
-
312
- state.idleTimer = setTimeout(() => {
313
- // Close idle connection
314
- try {
315
- state.ws.close(4000, 'Idle timeout');
316
- } catch {
317
- // Ignore close errors
318
- }
319
- this.unregister(state.ws);
320
- }, this.idleTimeoutMs);
321
- }
322
-
323
- /**
324
- * Close all connections.
325
- */
326
- async closeAll(): Promise<void> {
327
- const promises: Promise<void>[] = [];
328
-
329
- for (const [ws] of this.connections) {
330
- try {
331
- ws.close(1000, 'Server shutdown');
332
- } catch {
333
- // Ignore close errors
334
- }
335
- promises.push(this.unregister(ws));
336
- }
337
-
338
- await Promise.all(promises);
339
- }
340
- }