@databricks/appkit 0.1.4 → 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.
- package/AGENTS.md +89 -12
- package/CLAUDE.md +89 -12
- package/NOTICE.md +4 -0
- package/README.md +21 -15
- package/bin/appkit-lint.js +129 -0
- package/dist/analytics/analytics.d.ts +33 -8
- package/dist/analytics/analytics.d.ts.map +1 -1
- package/dist/analytics/analytics.js +67 -27
- package/dist/analytics/analytics.js.map +1 -1
- package/dist/analytics/defaults.js.map +1 -1
- package/dist/analytics/query.js +12 -6
- package/dist/analytics/query.js.map +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +7 -5
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/defaults.js.map +1 -1
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +25 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/storage/memory.js.map +1 -1
- package/dist/cache/storage/persistent.js +12 -6
- package/dist/cache/storage/persistent.js.map +1 -1
- package/dist/connectors/lakebase/client.js +31 -21
- package/dist/connectors/lakebase/client.js.map +1 -1
- package/dist/connectors/lakebase/defaults.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js +68 -28
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/defaults.js.map +1 -1
- package/dist/context/execution-context.js +75 -0
- package/dist/context/execution-context.js.map +1 -0
- package/dist/context/index.js +27 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/service-context.js +154 -0
- package/dist/context/service-context.js.map +1 -0
- package/dist/context/user-context.js +15 -0
- package/dist/context/user-context.js.map +1 -0
- package/dist/core/appkit.d.ts +3 -0
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +7 -0
- package/dist/core/appkit.js.map +1 -1
- package/dist/errors/authentication.d.ts +38 -0
- package/dist/errors/authentication.d.ts.map +1 -0
- package/dist/errors/authentication.js +48 -0
- package/dist/errors/authentication.js.map +1 -0
- package/dist/errors/base.d.ts +58 -0
- package/dist/errors/base.d.ts.map +1 -0
- package/dist/errors/base.js +70 -0
- package/dist/errors/base.js.map +1 -0
- package/dist/errors/configuration.d.ts +38 -0
- package/dist/errors/configuration.d.ts.map +1 -0
- package/dist/errors/configuration.js +45 -0
- package/dist/errors/configuration.js.map +1 -0
- package/dist/errors/connection.d.ts +42 -0
- package/dist/errors/connection.d.ts.map +1 -0
- package/dist/errors/connection.js +54 -0
- package/dist/errors/connection.js.map +1 -0
- package/dist/errors/execution.d.ts +42 -0
- package/dist/errors/execution.d.ts.map +1 -0
- package/dist/errors/execution.js +51 -0
- package/dist/errors/execution.js.map +1 -0
- package/dist/errors/index.js +28 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/initialization.d.ts +34 -0
- package/dist/errors/initialization.d.ts.map +1 -0
- package/dist/errors/initialization.js +42 -0
- package/dist/errors/initialization.js.map +1 -0
- package/dist/errors/server.d.ts +38 -0
- package/dist/errors/server.d.ts.map +1 -0
- package/dist/errors/server.js +45 -0
- package/dist/errors/server.js.map +1 -0
- package/dist/errors/tunnel.d.ts +38 -0
- package/dist/errors/tunnel.d.ts.map +1 -0
- package/dist/errors/tunnel.js +51 -0
- package/dist/errors/tunnel.js.map +1 -0
- package/dist/errors/validation.d.ts +36 -0
- package/dist/errors/validation.d.ts.map +1 -0
- package/dist/errors/validation.js +45 -0
- package/dist/errors/validation.js.map +1 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/logging/logger.js +179 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/sampling.js +56 -0
- package/dist/logging/sampling.js.map +1 -0
- package/dist/logging/wide-event-emitter.js +108 -0
- package/dist/logging/wide-event-emitter.js.map +1 -0
- package/dist/logging/wide-event.js +167 -0
- package/dist/logging/wide-event.js.map +1 -0
- package/dist/plugin/dev-reader.d.ts.map +1 -1
- package/dist/plugin/dev-reader.js +8 -3
- package/dist/plugin/dev-reader.js.map +1 -1
- package/dist/plugin/interceptors/cache.js.map +1 -1
- package/dist/plugin/interceptors/retry.js +10 -2
- package/dist/plugin/interceptors/retry.js.map +1 -1
- package/dist/plugin/interceptors/telemetry.js +24 -9
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/interceptors/timeout.js +4 -0
- package/dist/plugin/interceptors/timeout.js.map +1 -1
- package/dist/plugin/plugin.d.ts +38 -4
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +86 -5
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugin/to-plugin.d.ts +4 -0
- package/dist/plugin/to-plugin.d.ts.map +1 -1
- package/dist/plugin/to-plugin.js +3 -0
- package/dist/plugin/to-plugin.js.map +1 -1
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +25 -21
- package/dist/server/index.js.map +1 -1
- package/dist/server/remote-tunnel/remote-tunnel-controller.js +4 -2
- package/dist/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
- package/dist/server/remote-tunnel/remote-tunnel-manager.js +10 -8
- package/dist/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/server/utils.js.map +1 -1
- package/dist/server/vite-dev-server.js +8 -5
- package/dist/server/vite-dev-server.js.map +1 -1
- package/dist/shared/src/sql/helpers.js.map +1 -1
- package/dist/stream/arrow-stream-processor.js +13 -6
- package/dist/stream/arrow-stream-processor.js.map +1 -1
- package/dist/stream/buffers.js +5 -1
- package/dist/stream/buffers.js.map +1 -1
- package/dist/stream/sse-writer.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +47 -36
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/stream-registry.js.map +1 -1
- package/dist/stream/types.js.map +1 -1
- package/dist/telemetry/index.d.ts +2 -2
- package/dist/telemetry/index.js +2 -2
- package/dist/telemetry/instrumentations.js +14 -10
- package/dist/telemetry/instrumentations.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js +8 -6
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/telemetry/trace-sampler.js +33 -0
- package/dist/telemetry/trace-sampler.js.map +1 -0
- package/dist/type-generator/index.js +4 -2
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/query-registry.js +4 -2
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/types.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +5 -3
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/utils/env-validator.js +5 -5
- package/dist/utils/env-validator.js.map +1 -1
- package/dist/utils/merge.js +1 -5
- package/dist/utils/merge.js.map +1 -1
- package/dist/utils/path-exclusions.js +66 -0
- package/dist/utils/path-exclusions.js.map +1 -0
- package/dist/utils/vite-config-merge.js +1 -5
- package/dist/utils/vite-config-merge.js.map +1 -1
- package/llms.txt +89 -12
- package/package.json +6 -1
- package/dist/utils/databricks-client-middleware.d.ts +0 -17
- package/dist/utils/databricks-client-middleware.d.ts.map +0 -1
- package/dist/utils/databricks-client-middleware.js +0 -117
- package/dist/utils/databricks-client-middleware.js.map +0 -1
- package/dist/utils/index.js +0 -26
- package/dist/utils/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-tunnel-manager.js","names":["tunnelId: string | undefined","path","html"],"sources":["../../../src/server/remote-tunnel/remote-tunnel-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type express from \"express\";\nimport type { TunnelConnection } from \"shared\";\nimport { WebSocketServer } from \"ws\";\nimport {\n generateTunnelIdFromEmail,\n getConfigScript,\n parseCookies,\n} from \"../utils\";\nimport { REMOTE_TUNNEL_ASSET_PREFIXES } from \"./gate\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst MAX_ASSET_FETCH_TIMEOUT = 60_000;\n\ninterface DevFileReader {\n registerTunnelGetter(\n getter: (req: express.Request) => TunnelConnection | null,\n ): void;\n}\n\n/**\n * Remote tunnel manager for the AppKit.\n *\n * This class is responsible for managing the remote tunnels for the development server.\n * It also handles the asset fetching and the HMR for the development server.\n *\n * @example\n * ```ts\n * const remoteTunnelManager = new RemoteTunnelManager(devFileReader);\n * remoteTunnelManager.setup(app);\n * ```\n */\nexport class RemoteTunnelManager {\n private tunnels = new Map<string, TunnelConnection>();\n private wss: WebSocketServer;\n private hmrWss: WebSocketServer;\n private server?: HTTPServer;\n private devFileReader: DevFileReader;\n\n constructor(devFileReader: DevFileReader) {\n this.devFileReader = devFileReader;\n this.wss = new WebSocketServer({ noServer: true, path: \"/dev-tunnel\" });\n this.hmrWss = new WebSocketServer({ noServer: true, path: \"/dev-hmr\" });\n\n this.registerTunnelGetter();\n }\n\n setServer(server: HTTPServer) {\n this.server = server;\n }\n\n /** Asset middleware for the development server. */\n assetMiddleware() {\n return async (req: express.Request, res: express.Response) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n\n // Try cookie first, then generate from email\n let tunnelId: string | undefined;\n const cookieHeader = req.headers.cookie;\n\n if (cookieHeader) {\n // Fast path: extract dev-tunnel-id from cookie without full parse\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n if (!tunnelId) return res.status(404).send(\"Tunnel not ready\");\n\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) return res.status(404).send(\"Tunnel not found\");\n\n const { ws, approvedViewers, pendingFetches } = tunnel;\n\n if (!approvedViewers.has(email)) {\n return res.status(403).send(\"Not approved for this tunnel\");\n }\n\n const path = req.originalUrl;\n const requestId = randomUUID();\n\n const request = { type: \"fetch\", path, method: req.method, requestId };\n\n const response = await new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFetches.delete(requestId);\n reject(new Error(\"Asset fetch timeout\"));\n }, MAX_ASSET_FETCH_TIMEOUT);\n\n pendingFetches.set(requestId, { resolve, reject, timeout });\n\n ws.send(JSON.stringify(request));\n }).catch((err) => {\n console.error(`Failed to fetch ${path}:`, err.message);\n return { status: 504, body: Buffer.from(\"\"), headers: {} };\n });\n\n const r = response as any;\n\n res\n .status(r.status)\n .set(r.headers)\n .send(r.body || Buffer.from(\"\"));\n };\n }\n\n /** Dev mode middleware for the development server. */\n devModeMiddleware() {\n return async (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n const dev = req.query?.dev;\n\n if (dev === undefined) {\n return next();\n }\n\n if (\n req.path.startsWith(\"/api\") ||\n req.path.startsWith(\"/query\") ||\n req.path.match(/\\.(js|css|png|jpg|jpeg|svg|ico|json|woff|woff2|ttf)$/)\n ) {\n return next();\n }\n\n const viewerEmail = req.headers[\"x-forwarded-email\"] as string;\n const isOwnerMode = dev === \"\" || dev === \"true\";\n\n const tunnelId = isOwnerMode\n ? generateTunnelIdFromEmail(viewerEmail)\n : dev.toString();\n\n if (!tunnelId) {\n return res.status(400).send(\"Invalid tunnel ID\");\n }\n\n if (!isOwnerMode) {\n const approvalResponse = this.handleViewerApproval(\n tunnelId,\n viewerEmail,\n req.query.retry === \"true\",\n res,\n );\n\n if (approvalResponse) {\n return approvalResponse;\n }\n }\n\n res.cookie(\"dev-tunnel-id\", tunnelId, {\n httpOnly: false,\n sameSite: \"lax\",\n });\n\n const indexPath = path.join(__dirname, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${getConfigScript()}`);\n\n res.send(html);\n };\n }\n\n /** Setup the dev mode middleware. */\n setup(app: express.Application) {\n app.use(this.devModeMiddleware());\n app.use(REMOTE_TUNNEL_ASSET_PREFIXES, this.assetMiddleware());\n }\n\n static isRemoteServerEnabled() {\n return (\n process.env.NODE_ENV !== \"production\" &&\n process.env.DISABLE_REMOTE_SERVING !== \"true\" &&\n // DATABRICKS_CLIENT_SECRET is set in the .env file for deployed environments\n Boolean(process.env.DATABRICKS_CLIENT_SECRET)\n );\n }\n\n private loadHtmlTemplate(\n filename: string,\n replacements: Record<string, string>,\n ): string {\n const filePath = path.join(__dirname, filename);\n let content = fs.readFileSync(filePath, \"utf-8\");\n\n for (const [key, value] of Object.entries(replacements)) {\n content = content.replaceAll(`{{${key}}}`, value);\n }\n\n return content;\n }\n\n private handleViewerApproval(\n tunnelId: string,\n viewerEmail: string,\n retry: boolean,\n res: express.Response,\n ): express.Response | null {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) {\n return res.status(404).send(\"Tunnel not found\");\n }\n\n if (viewerEmail === tunnel.owner) {\n return null;\n }\n\n if (retry) {\n tunnel.rejectedViewers.delete(viewerEmail);\n }\n\n if (tunnel.rejectedViewers.has(viewerEmail)) {\n const html = this.loadHtmlTemplate(\"denied.html\", { tunnelId });\n return res.status(403).send(html);\n }\n\n if (tunnel.approvedViewers.has(viewerEmail)) {\n return null;\n }\n\n if (!tunnel.pendingRequests.has(viewerEmail)) {\n const requestId = randomUUID();\n tunnel.pendingRequests.add(viewerEmail);\n tunnel.ws.send(\n JSON.stringify({\n type: \"connection:request\",\n requestId,\n viewer: viewerEmail,\n }),\n );\n }\n\n const html = this.loadHtmlTemplate(\"wait.html\", { tunnelId });\n return res.status(200).send(html);\n }\n\n setupWebSocket() {\n this.wss.on(\"connection\", (ws, req) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId = generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return ws.close();\n\n this.tunnels.set(tunnelId, {\n ws,\n owner: email,\n approvedViewers: new Set([email]),\n pendingRequests: new Set(),\n rejectedViewers: new Set(),\n pendingFetches: new Map(),\n pendingFileReads: new Map(),\n waitingForBinaryBody: null,\n });\n\n ws.on(\"message\", (msg, isBinary) => {\n const tunnel = this.tunnels.get(tunnelId);\n if (!tunnel) return;\n\n if (isBinary) {\n if (!tunnel.waitingForBinaryBody) {\n console.warn(\n \"Received binary message but no requestId is waiting for body\",\n );\n return;\n }\n\n const requestId = tunnel.waitingForBinaryBody;\n const pending = tunnel.pendingFetches.get(requestId);\n\n if (!pending || !pending.metadata) {\n console.warn(\"Received binary message but pending fetch not found\");\n tunnel.waitingForBinaryBody = null;\n return;\n }\n\n tunnel.waitingForBinaryBody = null;\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(requestId);\n\n pending.resolve({\n status: pending.metadata.status,\n headers: pending.metadata.headers,\n body: msg as Buffer,\n });\n return;\n }\n\n try {\n const data = JSON.parse(msg.toString());\n\n if (data.type === \"connection:response\") {\n if (tunnel && data.viewer) {\n tunnel.pendingRequests.delete(data.viewer);\n\n if (data.approved) {\n tunnel.approvedViewers.add(data.viewer);\n console.log(\n `✅ Approved ${data.viewer} for tunnel ${tunnelId}`,\n );\n } else {\n tunnel.rejectedViewers.add(data.viewer);\n console.log(`❌ Denied ${data.viewer} for tunnel ${tunnelId}`);\n }\n }\n } else if (data.type === \"fetch:response:meta\") {\n const pending = tunnel.pendingFetches.get(data.requestId);\n if (pending) {\n pending.metadata = {\n status: data.status,\n headers: data.headers,\n };\n if (\n data.status === 304 ||\n data.status === 204 ||\n (data.status >= 300 && data.status < 400)\n ) {\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(data.requestId);\n pending.resolve({\n status: data.status,\n headers: data.headers,\n body: Buffer.from(\"\"),\n });\n } else {\n tunnel.waitingForBinaryBody = data.requestId;\n }\n }\n } else if (data.type === \"file:read:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else {\n pending.resolve(data.content);\n }\n }\n }\n } catch (e) {\n console.error(\"Failed to parse WebSocket message:\", e);\n }\n });\n\n ws.send(JSON.stringify({ type: \"tunnel:ready\", tunnelId }));\n\n ws.on(\"close\", () => {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (tunnel) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Tunnel closed\"));\n }\n tunnel.pendingFetches.clear();\n }\n\n this.tunnels.delete(tunnelId);\n });\n });\n\n this.hmrWss.on(\"connection\", (browserWs, req) => {\n const cookies = parseCookies(req);\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId =\n cookies[\"dev-tunnel-id\"] || generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return browserWs.close();\n\n const cliTunnel = this.tunnels.get(tunnelId);\n\n if (!cliTunnel) return browserWs.close();\n\n const { ws: cliWs, approvedViewers } = cliTunnel;\n\n if (!approvedViewers.has(email)) {\n return browserWs.close(1008, \"Not approved\");\n }\n // Browser → CLI\n browserWs.on(\"message\", (msg) => {\n const hmrStart = Date.now();\n console.log(\"browser -> cli browserWS message\", msg.toString());\n cliWs.send(\n JSON.stringify({\n type: \"hmr:message\",\n body: msg.toString(),\n timestamp: hmrStart,\n }),\n );\n });\n\n // // CLI → Browser\n const cliHandler = (msg: Buffer | string, isBinary: boolean) => {\n // Ignore binary messages (they're for fetch responses, not HMR)\n if (isBinary) return;\n\n try {\n const data = JSON.parse(msg.toString());\n\n if (data.type === \"hmr:message\") {\n browserWs.send(data.body);\n }\n } catch {\n console.error(\n \"Failed to parse CLI message for HMR:\",\n msg.toString().substring(0, 100),\n );\n }\n };\n cliWs.on(\"message\", cliHandler);\n\n browserWs.on(\"close\", () => {\n cliWs.off(\"message\", cliHandler);\n });\n });\n\n // // Browser HMR connection\n this.server?.on(\"upgrade\", (req, socket, head) => {\n const url = req.url ?? \"\";\n\n if (url.startsWith(\"/dev-tunnel\")) {\n this.wss.handleUpgrade(req, socket, head, (ws) => {\n this.wss.emit(\"connection\", ws, req);\n });\n } else if (url.startsWith(\"/dev-hmr\")) {\n this.hmrWss.handleUpgrade(req, socket, head, (browserWs) => {\n this.hmrWss.emit(\"connection\", browserWs, req);\n });\n }\n });\n }\n\n registerTunnelGetter() {\n this.devFileReader.registerTunnelGetter(\n this.getTunnelForRequest.bind(this),\n );\n }\n\n getTunnelForRequest(req: express.Request) {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const cookieHeader = req.headers.cookie;\n\n let tunnelId: string | undefined;\n\n if (cookieHeader) {\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n return tunnelId ? this.tunnels.get(tunnelId) || null : null;\n }\n\n cleanup() {\n for (const [, tunnel] of this.tunnels) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Server shutting down\"));\n }\n tunnel.pendingFetches.clear();\n tunnel.ws.close();\n }\n this.tunnels.clear();\n\n if (this.wss) {\n this.wss.close();\n }\n if (this.hmrWss) {\n this.hmrWss.close();\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"remote-tunnel-manager.js","names":["path","html"],"sources":["../../../src/server/remote-tunnel/remote-tunnel-manager.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type { Server as HTTPServer } from \"node:http\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type express from \"express\";\nimport type { TunnelConnection } from \"shared\";\nimport { WebSocketServer } from \"ws\";\nimport { createLogger } from \"../../logging/logger\";\nimport {\n generateTunnelIdFromEmail,\n getConfigScript,\n parseCookies,\n} from \"../utils\";\nimport { REMOTE_TUNNEL_ASSET_PREFIXES } from \"./gate\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\nconst MAX_ASSET_FETCH_TIMEOUT = 60_000;\n\nconst logger = createLogger(\"server:remote-tunnel\");\n\ninterface DevFileReader {\n registerTunnelGetter(\n getter: (req: express.Request) => TunnelConnection | null,\n ): void;\n}\n\n/**\n * Remote tunnel manager for the AppKit.\n *\n * This class is responsible for managing the remote tunnels for the development server.\n * It also handles the asset fetching and the HMR for the development server.\n *\n * @example\n * ```ts\n * const remoteTunnelManager = new RemoteTunnelManager(devFileReader);\n * remoteTunnelManager.setup(app);\n * ```\n */\nexport class RemoteTunnelManager {\n private tunnels = new Map<string, TunnelConnection>();\n private wss: WebSocketServer;\n private hmrWss: WebSocketServer;\n private server?: HTTPServer;\n private devFileReader: DevFileReader;\n\n constructor(devFileReader: DevFileReader) {\n this.devFileReader = devFileReader;\n this.wss = new WebSocketServer({ noServer: true, path: \"/dev-tunnel\" });\n this.hmrWss = new WebSocketServer({ noServer: true, path: \"/dev-hmr\" });\n\n this.registerTunnelGetter();\n }\n\n setServer(server: HTTPServer) {\n this.server = server;\n }\n\n /** Asset middleware for the development server. */\n assetMiddleware() {\n return async (req: express.Request, res: express.Response) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n\n // Try cookie first, then generate from email\n let tunnelId: string | undefined;\n const cookieHeader = req.headers.cookie;\n\n if (cookieHeader) {\n // Fast path: extract dev-tunnel-id from cookie without full parse\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n if (!tunnelId) return res.status(404).send(\"Tunnel not ready\");\n\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) return res.status(404).send(\"Tunnel not found\");\n\n const { ws, approvedViewers, pendingFetches } = tunnel;\n\n if (!approvedViewers.has(email)) {\n return res.status(403).send(\"Not approved for this tunnel\");\n }\n\n const path = req.originalUrl;\n const requestId = randomUUID();\n\n const request = { type: \"fetch\", path, method: req.method, requestId };\n\n const response = await new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFetches.delete(requestId);\n reject(new Error(\"Asset fetch timeout\"));\n }, MAX_ASSET_FETCH_TIMEOUT);\n\n pendingFetches.set(requestId, { resolve, reject, timeout });\n\n ws.send(JSON.stringify(request));\n }).catch((err) => {\n logger.error(\"Failed to fetch %s: %s\", path, err.message);\n return { status: 504, body: Buffer.from(\"\"), headers: {} };\n });\n\n const r = response as any;\n\n res\n .status(r.status)\n .set(r.headers)\n .send(r.body || Buffer.from(\"\"));\n };\n }\n\n /** Dev mode middleware for the development server. */\n devModeMiddleware() {\n return async (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n const dev = req.query?.dev;\n\n if (dev === undefined) {\n return next();\n }\n\n if (\n req.path.startsWith(\"/api\") ||\n req.path.startsWith(\"/query\") ||\n req.path.match(/\\.(js|css|png|jpg|jpeg|svg|ico|json|woff|woff2|ttf)$/)\n ) {\n return next();\n }\n\n const viewerEmail = req.headers[\"x-forwarded-email\"] as string;\n const isOwnerMode = dev === \"\" || dev === \"true\";\n\n const tunnelId = isOwnerMode\n ? generateTunnelIdFromEmail(viewerEmail)\n : dev.toString();\n\n if (!tunnelId) {\n return res.status(400).send(\"Invalid tunnel ID\");\n }\n\n if (!isOwnerMode) {\n const approvalResponse = this.handleViewerApproval(\n tunnelId,\n viewerEmail,\n req.query.retry === \"true\",\n res,\n );\n\n if (approvalResponse) {\n return approvalResponse;\n }\n }\n\n res.cookie(\"dev-tunnel-id\", tunnelId, {\n httpOnly: false,\n sameSite: \"lax\",\n });\n\n const indexPath = path.join(__dirname, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${getConfigScript()}`);\n\n res.send(html);\n };\n }\n\n /** Setup the dev mode middleware. */\n setup(app: express.Application) {\n app.use(this.devModeMiddleware());\n app.use(REMOTE_TUNNEL_ASSET_PREFIXES, this.assetMiddleware());\n }\n\n static isRemoteServerEnabled() {\n return (\n process.env.NODE_ENV !== \"production\" &&\n process.env.DISABLE_REMOTE_SERVING !== \"true\" &&\n // DATABRICKS_CLIENT_SECRET is set in the .env file for deployed environments\n Boolean(process.env.DATABRICKS_CLIENT_SECRET)\n );\n }\n\n private loadHtmlTemplate(\n filename: string,\n replacements: Record<string, string>,\n ): string {\n const filePath = path.join(__dirname, filename);\n let content = fs.readFileSync(filePath, \"utf-8\");\n\n for (const [key, value] of Object.entries(replacements)) {\n content = content.replaceAll(`{{${key}}}`, value);\n }\n\n return content;\n }\n\n private handleViewerApproval(\n tunnelId: string,\n viewerEmail: string,\n retry: boolean,\n res: express.Response,\n ): express.Response | null {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (!tunnel) {\n return res.status(404).send(\"Tunnel not found\");\n }\n\n if (viewerEmail === tunnel.owner) {\n return null;\n }\n\n if (retry) {\n tunnel.rejectedViewers.delete(viewerEmail);\n }\n\n if (tunnel.rejectedViewers.has(viewerEmail)) {\n const html = this.loadHtmlTemplate(\"denied.html\", { tunnelId });\n return res.status(403).send(html);\n }\n\n if (tunnel.approvedViewers.has(viewerEmail)) {\n return null;\n }\n\n if (!tunnel.pendingRequests.has(viewerEmail)) {\n const requestId = randomUUID();\n tunnel.pendingRequests.add(viewerEmail);\n tunnel.ws.send(\n JSON.stringify({\n type: \"connection:request\",\n requestId,\n viewer: viewerEmail,\n }),\n );\n }\n\n const html = this.loadHtmlTemplate(\"wait.html\", { tunnelId });\n return res.status(200).send(html);\n }\n\n setupWebSocket() {\n this.wss.on(\"connection\", (ws, req) => {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId = generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return ws.close();\n\n this.tunnels.set(tunnelId, {\n ws,\n owner: email,\n approvedViewers: new Set([email]),\n pendingRequests: new Set(),\n rejectedViewers: new Set(),\n pendingFetches: new Map(),\n pendingFileReads: new Map(),\n waitingForBinaryBody: null,\n });\n\n ws.on(\"message\", (msg, isBinary) => {\n const tunnel = this.tunnels.get(tunnelId);\n if (!tunnel) return;\n\n if (isBinary) {\n if (!tunnel.waitingForBinaryBody) {\n logger.debug(\n \"Received binary message but no requestId is waiting for body\",\n );\n return;\n }\n\n const requestId = tunnel.waitingForBinaryBody;\n const pending = tunnel.pendingFetches.get(requestId);\n\n if (!pending || !pending.metadata) {\n logger.debug(\"Received binary message but pending fetch not found\");\n tunnel.waitingForBinaryBody = null;\n return;\n }\n\n tunnel.waitingForBinaryBody = null;\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(requestId);\n\n pending.resolve({\n status: pending.metadata.status,\n headers: pending.metadata.headers,\n body: msg as Buffer,\n });\n return;\n }\n\n try {\n const data = JSON.parse(msg.toString());\n\n if (data.type === \"connection:response\") {\n if (tunnel && data.viewer) {\n tunnel.pendingRequests.delete(data.viewer);\n\n if (data.approved) {\n tunnel.approvedViewers.add(data.viewer);\n logger.debug(\n \"✅ Approved %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n } else {\n tunnel.rejectedViewers.add(data.viewer);\n logger.debug(\n \"❌ Denied %s for tunnel %s\",\n data.viewer,\n tunnelId,\n );\n }\n }\n } else if (data.type === \"fetch:response:meta\") {\n const pending = tunnel.pendingFetches.get(data.requestId);\n if (pending) {\n pending.metadata = {\n status: data.status,\n headers: data.headers,\n };\n if (\n data.status === 304 ||\n data.status === 204 ||\n (data.status >= 300 && data.status < 400)\n ) {\n clearTimeout(pending.timeout);\n tunnel.pendingFetches.delete(data.requestId);\n pending.resolve({\n status: data.status,\n headers: data.headers,\n body: Buffer.from(\"\"),\n });\n } else {\n tunnel.waitingForBinaryBody = data.requestId;\n }\n }\n } else if (data.type === \"file:read:response\") {\n const pending = tunnel.pendingFileReads.get(data.requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n tunnel.pendingFileReads.delete(data.requestId);\n\n if (data.error) {\n pending.reject(new Error(data.error));\n } else {\n pending.resolve(data.content);\n }\n }\n }\n } catch (e) {\n logger.error(\"Failed to parse WebSocket message: %O\", e);\n }\n });\n\n ws.send(JSON.stringify({ type: \"tunnel:ready\", tunnelId }));\n\n ws.on(\"close\", () => {\n const tunnel = this.tunnels.get(tunnelId);\n\n if (tunnel) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Tunnel closed\"));\n }\n tunnel.pendingFetches.clear();\n }\n\n this.tunnels.delete(tunnelId);\n });\n });\n\n this.hmrWss.on(\"connection\", (browserWs, req) => {\n const cookies = parseCookies(req);\n const email = req.headers[\"x-forwarded-email\"] as string;\n const tunnelId =\n cookies[\"dev-tunnel-id\"] || generateTunnelIdFromEmail(email);\n\n if (!tunnelId) return browserWs.close();\n\n const cliTunnel = this.tunnels.get(tunnelId);\n\n if (!cliTunnel) return browserWs.close();\n\n const { ws: cliWs, approvedViewers } = cliTunnel;\n\n if (!approvedViewers.has(email)) {\n return browserWs.close(1008, \"Not approved\");\n }\n // Browser → CLI\n browserWs.on(\"message\", (msg) => {\n const hmrStart = Date.now();\n logger.debug(\"browser -> cli browserWS message: %s\", msg.toString());\n cliWs.send(\n JSON.stringify({\n type: \"hmr:message\",\n body: msg.toString(),\n timestamp: hmrStart,\n }),\n );\n });\n\n // // CLI → Browser\n const cliHandler = (msg: Buffer | string, isBinary: boolean) => {\n // Ignore binary messages (they're for fetch responses, not HMR)\n if (isBinary) return;\n\n try {\n const data = JSON.parse(msg.toString());\n\n if (data.type === \"hmr:message\") {\n browserWs.send(data.body);\n }\n } catch {\n logger.error(\n \"Failed to parse CLI message for HMR: %s\",\n msg.toString().substring(0, 100),\n );\n }\n };\n cliWs.on(\"message\", cliHandler);\n\n browserWs.on(\"close\", () => {\n cliWs.off(\"message\", cliHandler);\n });\n });\n\n // // Browser HMR connection\n this.server?.on(\"upgrade\", (req, socket, head) => {\n const url = req.url ?? \"\";\n\n if (url.startsWith(\"/dev-tunnel\")) {\n this.wss.handleUpgrade(req, socket, head, (ws) => {\n this.wss.emit(\"connection\", ws, req);\n });\n } else if (url.startsWith(\"/dev-hmr\")) {\n this.hmrWss.handleUpgrade(req, socket, head, (browserWs) => {\n this.hmrWss.emit(\"connection\", browserWs, req);\n });\n }\n });\n }\n\n registerTunnelGetter() {\n this.devFileReader.registerTunnelGetter(\n this.getTunnelForRequest.bind(this),\n );\n }\n\n getTunnelForRequest(req: express.Request) {\n const email = req.headers[\"x-forwarded-email\"] as string;\n const cookieHeader = req.headers.cookie;\n\n let tunnelId: string | undefined;\n\n if (cookieHeader) {\n const match = cookieHeader.match(/dev-tunnel-id=([^;]+)/);\n if (match) {\n tunnelId = match[1];\n }\n }\n\n if (!tunnelId) {\n tunnelId = generateTunnelIdFromEmail(email);\n }\n\n return tunnelId ? this.tunnels.get(tunnelId) || null : null;\n }\n\n cleanup() {\n for (const [, tunnel] of this.tunnels) {\n for (const [_, pending] of tunnel.pendingFetches) {\n clearTimeout(pending.timeout);\n pending.reject(new Error(\"Server shutting down\"));\n }\n tunnel.pendingFetches.clear();\n tunnel.ws.close();\n }\n this.tunnels.clear();\n\n if (this.wss) {\n this.wss.close();\n }\n if (this.hmrWss) {\n this.hmrWss.close();\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/server/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":["
|
|
1
|
+
{"version":3,"file":"utils.js","names":["path"],"sources":["../../src/server/utils.ts"],"sourcesContent":["import crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport type http from \"node:http\";\nimport path from \"node:path\";\n\nexport function parseCookies(\n req: http.IncomingMessage,\n): Record<string, string> {\n const cookieHeader = req.headers.cookie;\n if (!cookieHeader) return {};\n\n // Fast path: if there's no semicolon, there's only one cookie\n const semicolonIndex = cookieHeader.indexOf(\";\");\n if (semicolonIndex === -1) {\n const eqIndex = cookieHeader.indexOf(\"=\");\n if (eqIndex === -1) return {};\n return {\n [cookieHeader.slice(0, eqIndex).trim()]: cookieHeader.slice(eqIndex + 1),\n };\n }\n\n // Multiple cookies: parse them all\n const cookies: Record<string, string> = {};\n const parts = cookieHeader.split(\";\");\n for (let i = 0; i < parts.length; i++) {\n const eqIndex = parts[i].indexOf(\"=\");\n if (eqIndex !== -1) {\n const key = parts[i].slice(0, eqIndex).trim();\n const value = parts[i].slice(eqIndex + 1);\n cookies[key] = value;\n }\n }\n return cookies;\n}\n\nexport function generateTunnelIdFromEmail(email?: string): string | undefined {\n if (!email) return undefined;\n\n const tunnelId = crypto\n .createHash(\"sha256\")\n .update(email)\n .digest(\"base64url\")\n .slice(0, 8);\n\n return tunnelId;\n}\n\nexport function getRoutes(stack: unknown[], basePath = \"\") {\n const routes: Array<{ path: string; methods: string[] }> = [];\n\n stack.forEach((layer: any) => {\n if (layer.route) {\n // normal route\n const path = basePath + layer.route.path;\n const methods = Object.keys(layer.route.methods).map((m) =>\n m.toUpperCase(),\n );\n routes.push({ path, methods });\n } else if (layer.name === \"router\" && layer.handle.stack) {\n // nested router\n const nestedBase =\n basePath +\n layer.regexp.source\n .replace(\"^\\\\\", \"\")\n .replace(\"\\\\/?(?=\\\\/|$)\", \"\")\n .replace(/\\\\\\//g, \"/\") // convert escaped slashes\n .replace(/\\$$/, \"\") || \"\";\n routes.push(...getRoutes(layer.handle.stack, nestedBase));\n }\n });\n\n return routes;\n}\n\nexport function getQueries(configFolder: string) {\n const queriesFolder = path.join(configFolder, \"queries\");\n\n if (!fs.existsSync(queriesFolder)) {\n return {};\n }\n\n return Object.fromEntries(\n fs\n .readdirSync(queriesFolder)\n .filter((f) => path.extname(f) === \".sql\")\n .map((f) => [path.basename(f, \".sql\"), path.basename(f, \".sql\")]),\n );\n}\n\nimport type { PluginEndpoints } from \"shared\";\n\nexport type { PluginEndpoints };\n\nexport interface RuntimeConfig {\n appName: string;\n queries: Record<string, string>;\n endpoints: PluginEndpoints;\n}\n\nexport function getRuntimeConfig(\n endpoints: PluginEndpoints = {},\n): RuntimeConfig {\n const configFolder = path.join(process.cwd(), \"config\");\n\n return {\n appName: process.env.DATABRICKS_APP_NAME || \"\",\n queries: getQueries(configFolder),\n endpoints,\n };\n}\n\nexport function getConfigScript(endpoints: PluginEndpoints = {}): string {\n const config = getRuntimeConfig(endpoints);\n\n return `\n <script>\n window.__CONFIG__ = ${JSON.stringify(config)};\n </script>\n `;\n}\n"],"mappings":";;;;;AAKA,SAAgB,aACd,KACwB;CACxB,MAAM,eAAe,IAAI,QAAQ;AACjC,KAAI,CAAC,aAAc,QAAO,EAAE;AAI5B,KADuB,aAAa,QAAQ,IAAI,KACzB,IAAI;EACzB,MAAM,UAAU,aAAa,QAAQ,IAAI;AACzC,MAAI,YAAY,GAAI,QAAO,EAAE;AAC7B,SAAO,GACJ,aAAa,MAAM,GAAG,QAAQ,CAAC,MAAM,GAAG,aAAa,MAAM,UAAU,EAAE,EACzE;;CAIH,MAAM,UAAkC,EAAE;CAC1C,MAAM,QAAQ,aAAa,MAAM,IAAI;AACrC,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,UAAU,MAAM,GAAG,QAAQ,IAAI;AACrC,MAAI,YAAY,IAAI;GAClB,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC,MAAM;AAE7C,WAAQ,OADM,MAAM,GAAG,MAAM,UAAU,EAAE;;;AAI7C,QAAO;;AAGT,SAAgB,0BAA0B,OAAoC;AAC5E,KAAI,CAAC,MAAO,QAAO;AAQnB,QANiB,OACd,WAAW,SAAS,CACpB,OAAO,MAAM,CACb,OAAO,YAAY,CACnB,MAAM,GAAG,EAAE;;AAKhB,SAAgB,UAAU,OAAkB,WAAW,IAAI;CACzD,MAAM,SAAqD,EAAE;AAE7D,OAAM,SAAS,UAAe;AAC5B,MAAI,MAAM,OAAO;GAEf,MAAMA,SAAO,WAAW,MAAM,MAAM;GACpC,MAAM,UAAU,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC,KAAK,MACpD,EAAE,aAAa,CAChB;AACD,UAAO,KAAK;IAAE;IAAM;IAAS,CAAC;aACrB,MAAM,SAAS,YAAY,MAAM,OAAO,OAAO;GAExD,MAAM,aACJ,WACE,MAAM,OAAO,OACV,QAAQ,OAAO,GAAG,CAClB,QAAQ,iBAAiB,GAAG,CAC5B,QAAQ,SAAS,IAAI,CACrB,QAAQ,OAAO,GAAG,IAAI;AAC7B,UAAO,KAAK,GAAG,UAAU,MAAM,OAAO,OAAO,WAAW,CAAC;;GAE3D;AAEF,QAAO;;AAGT,SAAgB,WAAW,cAAsB;CAC/C,MAAM,gBAAgB,KAAK,KAAK,cAAc,UAAU;AAExD,KAAI,CAAC,GAAG,WAAW,cAAc,CAC/B,QAAO,EAAE;AAGX,QAAO,OAAO,YACZ,GACG,YAAY,cAAc,CAC1B,QAAQ,MAAM,KAAK,QAAQ,EAAE,KAAK,OAAO,CACzC,KAAK,MAAM,CAAC,KAAK,SAAS,GAAG,OAAO,EAAE,KAAK,SAAS,GAAG,OAAO,CAAC,CAAC,CACpE;;AAaH,SAAgB,iBACd,YAA6B,EAAE,EAChB;CACf,MAAM,eAAe,KAAK,KAAK,QAAQ,KAAK,EAAE,SAAS;AAEvD,QAAO;EACL,SAAS,QAAQ,IAAI,uBAAuB;EAC5C,SAAS,WAAW,aAAa;EACjC;EACD;;AAGH,SAAgB,gBAAgB,YAA6B,EAAE,EAAU;CACvE,MAAM,SAAS,iBAAiB,UAAU;AAE1C,QAAO;;4BAEmB,KAAK,UAAU,OAAO,CAAC"}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { createLogger } from "../logging/logger.js";
|
|
2
|
+
import { ServerError } from "../errors/server.js";
|
|
3
|
+
import { init_errors } from "../errors/index.js";
|
|
1
4
|
import { mergeConfigDedup } from "../utils/vite-config-merge.js";
|
|
2
|
-
import { init_utils } from "../utils/index.js";
|
|
3
5
|
import { BaseServer } from "./base-server.js";
|
|
4
6
|
import { appKitTypesPlugin } from "../type-generator/vite-plugin.js";
|
|
5
7
|
import path from "node:path";
|
|
6
8
|
import fs from "node:fs";
|
|
7
9
|
|
|
8
10
|
//#region src/server/vite-dev-server.ts
|
|
9
|
-
|
|
11
|
+
init_errors();
|
|
12
|
+
const logger = createLogger("server:vite");
|
|
10
13
|
/**
|
|
11
14
|
* Vite dev server for the AppKit.
|
|
12
15
|
*
|
|
@@ -87,14 +90,14 @@ var ViteDevServer = class extends BaseServer {
|
|
|
87
90
|
const hasViteConfig = fs.existsSync(path.join(fullPath, "vite.config.ts")) || fs.existsSync(path.join(fullPath, "vite.config.js"));
|
|
88
91
|
const hasIndexHtml = fs.existsSync(path.join(fullPath, "index.html"));
|
|
89
92
|
if (hasViteConfig && hasIndexHtml) {
|
|
90
|
-
|
|
93
|
+
logger.debug("Vite dev server: using client root %s", fullPath);
|
|
91
94
|
return fullPath;
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
|
-
throw
|
|
97
|
+
throw ServerError.clientDirectoryNotFound(candidates);
|
|
95
98
|
}
|
|
96
99
|
validateVite(vite) {
|
|
97
|
-
if (!vite) throw
|
|
100
|
+
if (!vite) throw ServerError.viteNotInitialized();
|
|
98
101
|
}
|
|
99
102
|
};
|
|
100
103
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-dev-server.js","names":[],"sources":["../../src/server/vite-dev-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type express from \"express\";\nimport type { ViteDevServer as ViteDevServerType } from \"vite\";\nimport { mergeConfigDedup } from \"@/utils\";\nimport {
|
|
1
|
+
{"version":3,"file":"vite-dev-server.js","names":[],"sources":["../../src/server/vite-dev-server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type express from \"express\";\nimport type { ViteDevServer as ViteDevServerType } from \"vite\";\nimport { mergeConfigDedup } from \"@/utils\";\nimport { ServerError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { appKitTypesPlugin } from \"../type-generator/vite-plugin\";\nimport { BaseServer } from \"./base-server\";\nimport type { PluginEndpoints } from \"./utils\";\n\nconst logger = createLogger(\"server:vite\");\n\n/**\n * Vite dev server for the AppKit.\n *\n * This class is responsible for serving the Vite dev server for the development server.\n * It also handles the index.html file for the development server.\n *\n * @example\n * ```ts\n * const viteDevServer = new ViteDevServer(app, endpoints);\n * await viteDevServer.setup();\n * ```\n */\nexport class ViteDevServer extends BaseServer {\n private vite: ViteDevServerType | null;\n\n constructor(app: express.Application, endpoints: PluginEndpoints = {}) {\n super(app, endpoints);\n this.vite = null;\n }\n\n /**\n * Setup the Vite dev server.\n *\n * This method sets up the Vite dev server and the index.html file for the development server.\n *\n * @returns\n */\n async setup() {\n const {\n createServer: createViteServer,\n loadConfigFromFile,\n mergeConfig,\n } = await import(\"vite\");\n const react = await import(\"@vitejs/plugin-react\");\n\n const clientRoot = this.findClientRoot();\n\n const loadedConfig = await loadConfigFromFile(\n {\n mode: \"development\",\n command: \"serve\",\n },\n undefined,\n clientRoot,\n );\n\n const userConfig = loadedConfig?.config ?? {};\n const coreConfig = {\n configFile: false,\n root: clientRoot,\n server: {\n middlewareMode: true,\n watch: {\n useFsEvents: true,\n ignored: [\"**/node_modules/**\", \"!**/node_modules/@databricks/**\"],\n },\n },\n plugins: [react.default(), appKitTypesPlugin()],\n appType: \"custom\",\n };\n\n const mergedConfigs = mergeConfigDedup(userConfig, coreConfig, mergeConfig);\n this.vite = await createViteServer(mergedConfigs);\n\n this.app.use(this.vite.middlewares);\n\n this.app.use(\"*\", async (req, res, next) => {\n if (\n req.originalUrl.startsWith(\"/api\") ||\n req.originalUrl.startsWith(\"/query\")\n ) {\n return next();\n }\n const vite = this.vite;\n this.validateVite(vite);\n\n try {\n const indexPath = path.resolve(clientRoot, \"index.html\");\n let html = fs.readFileSync(indexPath, \"utf-8\");\n html = html.replace(\"<body>\", `<body>${this.getConfigScript()}`);\n html = await vite.transformIndexHtml(req.originalUrl, html);\n res.status(200).set({ \"Content-Type\": \"text/html\" }).end(html);\n } catch (e) {\n vite.ssrFixStacktrace(e as Error);\n next(e);\n }\n });\n }\n\n /** Close the Vite dev server. */\n async close() {\n await this.vite?.close();\n }\n\n /** Find the client root. */\n private findClientRoot(): string {\n const cwd = process.cwd();\n const candidates = [\"client\", \"src\", \"app\", \"frontend\", \".\"];\n\n for (const dir of candidates) {\n const fullPath = path.resolve(cwd, dir);\n const hasViteConfig =\n fs.existsSync(path.join(fullPath, \"vite.config.ts\")) ||\n fs.existsSync(path.join(fullPath, \"vite.config.js\"));\n const hasIndexHtml = fs.existsSync(path.join(fullPath, \"index.html\"));\n\n if (hasViteConfig && hasIndexHtml) {\n logger.debug(\"Vite dev server: using client root %s\", fullPath);\n return fullPath;\n }\n }\n\n throw ServerError.clientDirectoryNotFound(candidates);\n }\n\n // type assertion to ensure vite is not null\n private validateVite(\n vite: ViteDevServerType | null,\n ): asserts vite is ViteDevServerType {\n if (!vite) {\n throw ServerError.viteNotInitialized();\n }\n }\n}\n"],"mappings":";;;;;;;;;;aAKwC;AAMxC,MAAM,SAAS,aAAa,cAAc;;;;;;;;;;;;;AAc1C,IAAa,gBAAb,cAAmC,WAAW;CAG5C,YAAY,KAA0B,YAA6B,EAAE,EAAE;AACrE,QAAM,KAAK,UAAU;AACrB,OAAK,OAAO;;;;;;;;;CAUd,MAAM,QAAQ;EACZ,MAAM,EACJ,cAAc,kBACd,oBACA,gBACE,MAAM,OAAO;EACjB,MAAM,QAAQ,MAAM,OAAO;EAE3B,MAAM,aAAa,KAAK,gBAAgB;AA2BxC,OAAK,OAAO,MAAM,iBADI,kBAxBD,MAAM,mBACzB;GACE,MAAM;GACN,SAAS;GACV,EACD,QACA,WACD,GAEgC,UAAU,EAAE,EAC1B;GACjB,YAAY;GACZ,MAAM;GACN,QAAQ;IACN,gBAAgB;IAChB,OAAO;KACL,aAAa;KACb,SAAS,CAAC,sBAAsB,kCAAkC;KACnE;IACF;GACD,SAAS,CAAC,MAAM,SAAS,EAAE,mBAAmB,CAAC;GAC/C,SAAS;GACV,EAE8D,YAAY,CAC1B;AAEjD,OAAK,IAAI,IAAI,KAAK,KAAK,YAAY;AAEnC,OAAK,IAAI,IAAI,KAAK,OAAO,KAAK,KAAK,SAAS;AAC1C,OACE,IAAI,YAAY,WAAW,OAAO,IAClC,IAAI,YAAY,WAAW,SAAS,CAEpC,QAAO,MAAM;GAEf,MAAM,OAAO,KAAK;AAClB,QAAK,aAAa,KAAK;AAEvB,OAAI;IACF,MAAM,YAAY,KAAK,QAAQ,YAAY,aAAa;IACxD,IAAI,OAAO,GAAG,aAAa,WAAW,QAAQ;AAC9C,WAAO,KAAK,QAAQ,UAAU,SAAS,KAAK,iBAAiB,GAAG;AAChE,WAAO,MAAM,KAAK,mBAAmB,IAAI,aAAa,KAAK;AAC3D,QAAI,OAAO,IAAI,CAAC,IAAI,EAAE,gBAAgB,aAAa,CAAC,CAAC,IAAI,KAAK;YACvD,GAAG;AACV,SAAK,iBAAiB,EAAW;AACjC,SAAK,EAAE;;IAET;;;CAIJ,MAAM,QAAQ;AACZ,QAAM,KAAK,MAAM,OAAO;;;CAI1B,AAAQ,iBAAyB;EAC/B,MAAM,MAAM,QAAQ,KAAK;EACzB,MAAM,aAAa;GAAC;GAAU;GAAO;GAAO;GAAY;GAAI;AAE5D,OAAK,MAAM,OAAO,YAAY;GAC5B,MAAM,WAAW,KAAK,QAAQ,KAAK,IAAI;GACvC,MAAM,gBACJ,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC,IACpD,GAAG,WAAW,KAAK,KAAK,UAAU,iBAAiB,CAAC;GACtD,MAAM,eAAe,GAAG,WAAW,KAAK,KAAK,UAAU,aAAa,CAAC;AAErE,OAAI,iBAAiB,cAAc;AACjC,WAAO,MAAM,yCAAyC,SAAS;AAC/D,WAAO;;;AAIX,QAAM,YAAY,wBAAwB,WAAW;;CAIvD,AAAQ,aACN,MACmC;AACnC,MAAI,CAAC,KACH,OAAM,YAAY,oBAAoB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.js","names":["dateValue: string","timestampValue: string","numValue: string","stringValue: string","booleanValue: string","hexValue: string"],"sources":["../../../../../shared/src/sql/helpers.ts"],"sourcesContent":["import type {\n SQLBinaryMarker,\n SQLBooleanMarker,\n SQLDateMarker,\n SQLNumberMarker,\n SQLStringMarker,\n SQLTimestampMarker,\n SQLTypeMarker,\n} from \"./types\";\n\n/**\n * SQL helper namespace\n */\nexport const sql = {\n /**\n * Creates a DATE type parameter\n * Accepts Date objects or ISO date strings (YYYY-MM-DD format)\n * @param value - Date object or ISO date string\n * @returns Marker object for DATE type parameter\n * @example\n * ```typescript\n * const params = { startDate: sql.date(new Date(\"2024-01-01\")) };\n * params = { startDate: \"2024-01-01\" }\n * ```\n * @example\n * ```typescript\n * const params = { startDate: sql.date(\"2024-01-01\") };\n * params = { startDate: \"2024-01-01\" }\n * ```\n */\n date(value: Date | string): SQLDateMarker {\n let dateValue: string = \"\";\n\n // check if value is a Date object\n if (value instanceof Date) {\n dateValue = value.toISOString().split(\"T\")[0];\n }\n // check if value is a string\n else if (typeof value === \"string\") {\n // validate format\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) {\n throw new Error(\n `sql.date() expects Date or ISO date string (YYYY-MM-DD format), got: ${value}`,\n );\n }\n dateValue = value;\n }\n // if value is not a Date object or string, throw an error\n else {\n throw new Error(\n `sql.date() expects Date or ISO date string (YYYY-MM-DD format), got: ${typeof value}`,\n );\n }\n\n return {\n __sql_type: \"DATE\",\n value: dateValue,\n };\n },\n\n /**\n * Creates a TIMESTAMP type parameter\n * Accepts Date objects, ISO timestamp strings, or Unix timestamp numbers\n * @param value - Date object, ISO timestamp string, or Unix timestamp number\n * @returns Marker object for TIMESTAMP type parameter\n * @example\n * ```typescript\n * const params = { createdTime: sql.timestamp(new Date(\"2024-01-01T12:00:00Z\")) };\n * params = { createdTime: \"2024-01-01T12:00:00Z\" }\n * ```\n * @example\n * ```typescript\n * const params = { createdTime: sql.timestamp(\"2024-01-01T12:00:00Z\") };\n * params = { createdTime: \"2024-01-01T12:00:00Z\" }\n * ```\n * @example\n * ```typescript\n * const params = { createdTime: sql.timestamp(1704110400000) };\n * params = { createdTime: \"2024-01-01T12:00:00Z\" }\n * ```\n */\n timestamp(value: Date | string | number): SQLTimestampMarker {\n let timestampValue: string = \"\";\n\n if (value instanceof Date) {\n timestampValue = value.toISOString().replace(/\\.000(Z|[+-])/, \"$1\");\n } else if (typeof value === \"string\") {\n const isoRegex =\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})?$/;\n if (!isoRegex.test(value)) {\n throw new Error(\n `sql.timestamp() expects ISO timestamp string (YYYY-MM-DDTHH:MM:SS.mmmZ or YYYY-MM-DDTHH:MM:SS.mmm+HH:MM), got: ${value}`,\n );\n }\n timestampValue = value;\n } else if (typeof value === \"number\") {\n const date = new Date(value > 1e12 ? value : value * 1000);\n timestampValue = date.toISOString().replace(/\\.000(Z|[+-])/, \"$1\");\n } else {\n throw new Error(\n `sql.timestamp() expects Date, ISO timestamp string, or Unix timestamp number, got: ${typeof value}`,\n );\n }\n\n return {\n __sql_type: \"TIMESTAMP\",\n value: timestampValue,\n };\n },\n\n /**\n * Creates a NUMERIC type parameter\n * Accepts numbers or numeric strings\n * @param value - Number or numeric string\n * @returns Marker object for NUMERIC type parameter\n * @example\n * ```typescript\n * const params = { userId: sql.number(123) };\n * params = { userId: \"123\" }\n * ```\n * @example\n * ```typescript\n * const params = { userId: sql.number(\"123\") };\n * params = { userId: \"123\" }\n * ```\n */\n number(value: number | string): SQLNumberMarker {\n let numValue: string = \"\";\n\n // check if value is a number\n if (typeof value === \"number\") {\n numValue = value.toString();\n }\n // check if value is a string\n else if (typeof value === \"string\") {\n if (value === \"\" || Number.isNaN(Number(value))) {\n throw new Error(\n `sql.number() expects number or numeric string, got: ${value === \"\" ? \"empty string\" : value}`,\n );\n }\n numValue = value;\n }\n // if value is not a number or string, throw an error\n else {\n throw new Error(\n `sql.number() expects number or numeric string, got: ${typeof value}`,\n );\n }\n\n return {\n __sql_type: \"NUMERIC\",\n value: numValue,\n };\n },\n\n /**\n * Creates a STRING type parameter\n * Accepts strings, numbers, or booleans\n * @param value - String, number, or boolean\n * @returns Marker object for STRING type parameter\n * @example\n * ```typescript\n * const params = { name: sql.string(\"John\") };\n * params = { name: \"John\" }\n * ```\n * @example\n * ```typescript\n * const params = { name: sql.string(123) };\n * params = { name: \"123\" }\n * ```\n * @example\n * ```typescript\n * const params = { name: sql.string(true) };\n * params = { name: \"true\" }\n * ```\n */\n string(value: string | number | boolean): SQLStringMarker {\n if (\n typeof value !== \"string\" &&\n typeof value !== \"number\" &&\n typeof value !== \"boolean\"\n ) {\n throw new Error(\n `sql.string() expects string or number or boolean, got: ${typeof value}`,\n );\n }\n\n let stringValue: string = \"\";\n\n if (typeof value === \"string\") {\n stringValue = value;\n } else {\n stringValue = value.toString();\n }\n\n return {\n __sql_type: \"STRING\",\n value: stringValue,\n };\n },\n\n /**\n * Create a BOOLEAN type parameter\n * Accepts booleans, strings, or numbers\n * @param value - Boolean, string, or number\n * @returns Marker object for BOOLEAN type parameter\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(true) };\n * params = { isActive: \"true\" }\n * ```\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(\"true\") };\n * params = { isActive: \"true\" }\n * ```\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(1) };\n * params = { isActive: \"true\" }\n * ```\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(\"false\") };\n * params = { isActive: \"false\" }\n * ```\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(0) };\n * params = { isActive: \"false\" }\n * ```\n * @returns\n */\n boolean(value: boolean | string | number): SQLBooleanMarker {\n if (\n typeof value !== \"boolean\" &&\n typeof value !== \"string\" &&\n typeof value !== \"number\"\n ) {\n throw new Error(\n `sql.boolean() expects boolean or string (true or false) or number (1 or 0), got: ${typeof value}`,\n );\n }\n\n let booleanValue: string = \"\";\n\n if (typeof value === \"boolean\") {\n booleanValue = value.toString();\n }\n // check if value is a number\n else if (typeof value === \"number\") {\n if (value !== 1 && value !== 0) {\n throw new Error(\n `sql.boolean() expects boolean or string (true or false) or number (1 or 0), got: ${value}`,\n );\n }\n booleanValue = value === 1 ? \"true\" : \"false\";\n }\n // check if value is a string\n else if (typeof value === \"string\") {\n if (value !== \"true\" && value !== \"false\") {\n throw new Error(\n `sql.boolean() expects boolean or string (true or false) or number (1 or 0), got: ${value}`,\n );\n }\n booleanValue = value;\n }\n\n return {\n __sql_type: \"BOOLEAN\",\n value: booleanValue,\n };\n },\n\n /**\n * Creates a BINARY parameter as hex-encoded STRING\n * Accepts Uint8Array, ArrayBuffer, or hex string\n * Note: Databricks SQL Warehouse doesn't support BINARY as parameter type,\n * so this helper returns a STRING with hex encoding. Use UNHEX(:param) in your SQL.\n * @param value - Uint8Array, ArrayBuffer, or hex string\n * @returns Marker object with STRING type and hex-encoded value\n * @example\n * ```typescript\n * // From Uint8Array:\n * const params = { data: sql.binary(new Uint8Array([0x53, 0x70, 0x61, 0x72, 0x6b])) };\n * // Returns: { __sql_type: \"STRING\", value: \"537061726B\" }\n * // SQL: SELECT UNHEX(:data) as binary_value\n * ```\n * @example\n * ```typescript\n * // From hex string:\n * const params = { data: sql.binary(\"537061726B\") };\n * // Returns: { __sql_type: \"STRING\", value: \"537061726B\" }\n * ```\n */\n binary(value: Uint8Array | ArrayBuffer | string): SQLBinaryMarker {\n let hexValue: string = \"\";\n\n if (value instanceof Uint8Array) {\n // if value is a Uint8Array, convert it to a hex string\n hexValue = Array.from(value)\n .map((b) => b.toString(16).padStart(2, \"0\").toUpperCase())\n .join(\"\");\n } else if (value instanceof ArrayBuffer) {\n // if value is an ArrayBuffer, convert it to a hex string\n hexValue = Array.from(new Uint8Array(value))\n .map((b) => b.toString(16).padStart(2, \"0\").toUpperCase())\n .join(\"\");\n } else if (typeof value === \"string\") {\n // validate hex string\n if (!/^[0-9A-Fa-f]*$/.test(value)) {\n throw new Error(\n `sql.binary() expects Uint8Array, ArrayBuffer, or hex string, got invalid hex: ${value}`,\n );\n }\n hexValue = value.toUpperCase();\n } else {\n throw new Error(\n `sql.binary() expects Uint8Array, ArrayBuffer, or hex string, got: ${typeof value}`,\n );\n }\n\n return {\n __sql_type: \"STRING\",\n value: hexValue,\n };\n },\n};\n\n/**\n * Type guard to check if a value is a SQL type marker\n * @param value - Value to check\n * @returns True if the value is a SQL type marker, false otherwise\n * @example\n * ```typescript\n * const value = {\n * __sql_type: \"DATE\",\n * value: \"2024-01-01\",\n * };\n * const isSQLTypeMarker = isSQLTypeMarker(value);\n * console.log(isSQLTypeMarker); // true\n * ```\n */\nexport function isSQLTypeMarker(value: any): value is SQLTypeMarker {\n return (\n value !== null &&\n typeof value === \"object\" &&\n \"__sql_type\" in value &&\n \"value\" in value &&\n typeof value.__sql_type === \"string\" &&\n typeof value.value === \"string\"\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"helpers.js","names":[],"sources":["../../../../../shared/src/sql/helpers.ts"],"sourcesContent":["import type {\n SQLBinaryMarker,\n SQLBooleanMarker,\n SQLDateMarker,\n SQLNumberMarker,\n SQLStringMarker,\n SQLTimestampMarker,\n SQLTypeMarker,\n} from \"./types\";\n\n/**\n * SQL helper namespace\n */\nexport const sql = {\n /**\n * Creates a DATE type parameter\n * Accepts Date objects or ISO date strings (YYYY-MM-DD format)\n * @param value - Date object or ISO date string\n * @returns Marker object for DATE type parameter\n * @example\n * ```typescript\n * const params = { startDate: sql.date(new Date(\"2024-01-01\")) };\n * params = { startDate: \"2024-01-01\" }\n * ```\n * @example\n * ```typescript\n * const params = { startDate: sql.date(\"2024-01-01\") };\n * params = { startDate: \"2024-01-01\" }\n * ```\n */\n date(value: Date | string): SQLDateMarker {\n let dateValue: string = \"\";\n\n // check if value is a Date object\n if (value instanceof Date) {\n dateValue = value.toISOString().split(\"T\")[0];\n }\n // check if value is a string\n else if (typeof value === \"string\") {\n // validate format\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) {\n throw new Error(\n `sql.date() expects Date or ISO date string (YYYY-MM-DD format), got: ${value}`,\n );\n }\n dateValue = value;\n }\n // if value is not a Date object or string, throw an error\n else {\n throw new Error(\n `sql.date() expects Date or ISO date string (YYYY-MM-DD format), got: ${typeof value}`,\n );\n }\n\n return {\n __sql_type: \"DATE\",\n value: dateValue,\n };\n },\n\n /**\n * Creates a TIMESTAMP type parameter\n * Accepts Date objects, ISO timestamp strings, or Unix timestamp numbers\n * @param value - Date object, ISO timestamp string, or Unix timestamp number\n * @returns Marker object for TIMESTAMP type parameter\n * @example\n * ```typescript\n * const params = { createdTime: sql.timestamp(new Date(\"2024-01-01T12:00:00Z\")) };\n * params = { createdTime: \"2024-01-01T12:00:00Z\" }\n * ```\n * @example\n * ```typescript\n * const params = { createdTime: sql.timestamp(\"2024-01-01T12:00:00Z\") };\n * params = { createdTime: \"2024-01-01T12:00:00Z\" }\n * ```\n * @example\n * ```typescript\n * const params = { createdTime: sql.timestamp(1704110400000) };\n * params = { createdTime: \"2024-01-01T12:00:00Z\" }\n * ```\n */\n timestamp(value: Date | string | number): SQLTimestampMarker {\n let timestampValue: string = \"\";\n\n if (value instanceof Date) {\n timestampValue = value.toISOString().replace(/\\.000(Z|[+-])/, \"$1\");\n } else if (typeof value === \"string\") {\n const isoRegex =\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})?$/;\n if (!isoRegex.test(value)) {\n throw new Error(\n `sql.timestamp() expects ISO timestamp string (YYYY-MM-DDTHH:MM:SS.mmmZ or YYYY-MM-DDTHH:MM:SS.mmm+HH:MM), got: ${value}`,\n );\n }\n timestampValue = value;\n } else if (typeof value === \"number\") {\n const date = new Date(value > 1e12 ? value : value * 1000);\n timestampValue = date.toISOString().replace(/\\.000(Z|[+-])/, \"$1\");\n } else {\n throw new Error(\n `sql.timestamp() expects Date, ISO timestamp string, or Unix timestamp number, got: ${typeof value}`,\n );\n }\n\n return {\n __sql_type: \"TIMESTAMP\",\n value: timestampValue,\n };\n },\n\n /**\n * Creates a NUMERIC type parameter\n * Accepts numbers or numeric strings\n * @param value - Number or numeric string\n * @returns Marker object for NUMERIC type parameter\n * @example\n * ```typescript\n * const params = { userId: sql.number(123) };\n * params = { userId: \"123\" }\n * ```\n * @example\n * ```typescript\n * const params = { userId: sql.number(\"123\") };\n * params = { userId: \"123\" }\n * ```\n */\n number(value: number | string): SQLNumberMarker {\n let numValue: string = \"\";\n\n // check if value is a number\n if (typeof value === \"number\") {\n numValue = value.toString();\n }\n // check if value is a string\n else if (typeof value === \"string\") {\n if (value === \"\" || Number.isNaN(Number(value))) {\n throw new Error(\n `sql.number() expects number or numeric string, got: ${value === \"\" ? \"empty string\" : value}`,\n );\n }\n numValue = value;\n }\n // if value is not a number or string, throw an error\n else {\n throw new Error(\n `sql.number() expects number or numeric string, got: ${typeof value}`,\n );\n }\n\n return {\n __sql_type: \"NUMERIC\",\n value: numValue,\n };\n },\n\n /**\n * Creates a STRING type parameter\n * Accepts strings, numbers, or booleans\n * @param value - String, number, or boolean\n * @returns Marker object for STRING type parameter\n * @example\n * ```typescript\n * const params = { name: sql.string(\"John\") };\n * params = { name: \"John\" }\n * ```\n * @example\n * ```typescript\n * const params = { name: sql.string(123) };\n * params = { name: \"123\" }\n * ```\n * @example\n * ```typescript\n * const params = { name: sql.string(true) };\n * params = { name: \"true\" }\n * ```\n */\n string(value: string | number | boolean): SQLStringMarker {\n if (\n typeof value !== \"string\" &&\n typeof value !== \"number\" &&\n typeof value !== \"boolean\"\n ) {\n throw new Error(\n `sql.string() expects string or number or boolean, got: ${typeof value}`,\n );\n }\n\n let stringValue: string = \"\";\n\n if (typeof value === \"string\") {\n stringValue = value;\n } else {\n stringValue = value.toString();\n }\n\n return {\n __sql_type: \"STRING\",\n value: stringValue,\n };\n },\n\n /**\n * Create a BOOLEAN type parameter\n * Accepts booleans, strings, or numbers\n * @param value - Boolean, string, or number\n * @returns Marker object for BOOLEAN type parameter\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(true) };\n * params = { isActive: \"true\" }\n * ```\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(\"true\") };\n * params = { isActive: \"true\" }\n * ```\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(1) };\n * params = { isActive: \"true\" }\n * ```\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(\"false\") };\n * params = { isActive: \"false\" }\n * ```\n * @example\n * ```typescript\n * const params = { isActive: sql.boolean(0) };\n * params = { isActive: \"false\" }\n * ```\n * @returns\n */\n boolean(value: boolean | string | number): SQLBooleanMarker {\n if (\n typeof value !== \"boolean\" &&\n typeof value !== \"string\" &&\n typeof value !== \"number\"\n ) {\n throw new Error(\n `sql.boolean() expects boolean or string (true or false) or number (1 or 0), got: ${typeof value}`,\n );\n }\n\n let booleanValue: string = \"\";\n\n if (typeof value === \"boolean\") {\n booleanValue = value.toString();\n }\n // check if value is a number\n else if (typeof value === \"number\") {\n if (value !== 1 && value !== 0) {\n throw new Error(\n `sql.boolean() expects boolean or string (true or false) or number (1 or 0), got: ${value}`,\n );\n }\n booleanValue = value === 1 ? \"true\" : \"false\";\n }\n // check if value is a string\n else if (typeof value === \"string\") {\n if (value !== \"true\" && value !== \"false\") {\n throw new Error(\n `sql.boolean() expects boolean or string (true or false) or number (1 or 0), got: ${value}`,\n );\n }\n booleanValue = value;\n }\n\n return {\n __sql_type: \"BOOLEAN\",\n value: booleanValue,\n };\n },\n\n /**\n * Creates a BINARY parameter as hex-encoded STRING\n * Accepts Uint8Array, ArrayBuffer, or hex string\n * Note: Databricks SQL Warehouse doesn't support BINARY as parameter type,\n * so this helper returns a STRING with hex encoding. Use UNHEX(:param) in your SQL.\n * @param value - Uint8Array, ArrayBuffer, or hex string\n * @returns Marker object with STRING type and hex-encoded value\n * @example\n * ```typescript\n * // From Uint8Array:\n * const params = { data: sql.binary(new Uint8Array([0x53, 0x70, 0x61, 0x72, 0x6b])) };\n * // Returns: { __sql_type: \"STRING\", value: \"537061726B\" }\n * // SQL: SELECT UNHEX(:data) as binary_value\n * ```\n * @example\n * ```typescript\n * // From hex string:\n * const params = { data: sql.binary(\"537061726B\") };\n * // Returns: { __sql_type: \"STRING\", value: \"537061726B\" }\n * ```\n */\n binary(value: Uint8Array | ArrayBuffer | string): SQLBinaryMarker {\n let hexValue: string = \"\";\n\n if (value instanceof Uint8Array) {\n // if value is a Uint8Array, convert it to a hex string\n hexValue = Array.from(value)\n .map((b) => b.toString(16).padStart(2, \"0\").toUpperCase())\n .join(\"\");\n } else if (value instanceof ArrayBuffer) {\n // if value is an ArrayBuffer, convert it to a hex string\n hexValue = Array.from(new Uint8Array(value))\n .map((b) => b.toString(16).padStart(2, \"0\").toUpperCase())\n .join(\"\");\n } else if (typeof value === \"string\") {\n // validate hex string\n if (!/^[0-9A-Fa-f]*$/.test(value)) {\n throw new Error(\n `sql.binary() expects Uint8Array, ArrayBuffer, or hex string, got invalid hex: ${value}`,\n );\n }\n hexValue = value.toUpperCase();\n } else {\n throw new Error(\n `sql.binary() expects Uint8Array, ArrayBuffer, or hex string, got: ${typeof value}`,\n );\n }\n\n return {\n __sql_type: \"STRING\",\n value: hexValue,\n };\n },\n};\n\n/**\n * Type guard to check if a value is a SQL type marker\n * @param value - Value to check\n * @returns True if the value is a SQL type marker, false otherwise\n * @example\n * ```typescript\n * const value = {\n * __sql_type: \"DATE\",\n * value: \"2024-01-01\",\n * };\n * const isSQLTypeMarker = isSQLTypeMarker(value);\n * console.log(isSQLTypeMarker); // true\n * ```\n */\nexport function isSQLTypeMarker(value: any): value is SQLTypeMarker {\n return (\n value !== null &&\n typeof value === \"object\" &&\n \"__sql_type\" in value &&\n \"value\" in value &&\n typeof value.__sql_type === \"string\" &&\n typeof value.value === \"string\"\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { createLogger } from "../logging/logger.js";
|
|
2
|
+
import { ExecutionError } from "../errors/execution.js";
|
|
3
|
+
import { ValidationError } from "../errors/validation.js";
|
|
4
|
+
import { init_errors } from "../errors/index.js";
|
|
5
|
+
|
|
1
6
|
//#region src/stream/arrow-stream-processor.ts
|
|
7
|
+
init_errors();
|
|
8
|
+
const logger = createLogger("stream:arrow");
|
|
2
9
|
const BACKOFF_MULTIPLIER = 1e3;
|
|
3
10
|
var ArrowStreamProcessor = class ArrowStreamProcessor {
|
|
4
11
|
static {
|
|
@@ -38,7 +45,7 @@ var ArrowStreamProcessor = class ArrowStreamProcessor {
|
|
|
38
45
|
* @returns Raw concatenated IPC bytes with schema
|
|
39
46
|
*/
|
|
40
47
|
async processChunks(chunks, schema, signal) {
|
|
41
|
-
if (chunks.length === 0) throw
|
|
48
|
+
if (chunks.length === 0) throw ValidationError.missingField("chunks");
|
|
42
49
|
const buffers = await this.downloadChunksRaw(chunks, signal);
|
|
43
50
|
return {
|
|
44
51
|
data: this.concatenateBuffers(buffers),
|
|
@@ -74,30 +81,30 @@ var ArrowStreamProcessor = class ArrowStreamProcessor {
|
|
|
74
81
|
try {
|
|
75
82
|
const externalLink = chunk.external_link;
|
|
76
83
|
if (!externalLink) {
|
|
77
|
-
|
|
84
|
+
logger.error("External link is required for chunk: %O", chunk);
|
|
78
85
|
continue;
|
|
79
86
|
}
|
|
80
87
|
const response = await fetch(externalLink, { signal: combinedSignal });
|
|
81
|
-
if (!response.ok) throw
|
|
88
|
+
if (!response.ok) throw ExecutionError.statementFailed(`Failed to download chunk ${chunk.chunk_index}: ${response.status} ${response.statusText}`);
|
|
82
89
|
const arrayBuffer = await response.arrayBuffer();
|
|
83
90
|
return new Uint8Array(arrayBuffer);
|
|
84
91
|
} catch (error) {
|
|
85
92
|
lastError = error;
|
|
86
93
|
if (timeoutController.signal.aborted) lastError = /* @__PURE__ */ new Error(`Chunk ${chunk.chunk_index} download timed out after ${this.options.timeout}ms`);
|
|
87
|
-
if (signal?.aborted) throw
|
|
94
|
+
if (signal?.aborted) throw ExecutionError.canceled();
|
|
88
95
|
if (attempt < this.options.retries - 1) await this.delay(2 ** attempt * BACKOFF_MULTIPLIER);
|
|
89
96
|
} finally {
|
|
90
97
|
clearTimeout(timeoutId);
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
|
-
throw
|
|
100
|
+
throw ExecutionError.statementFailed(`Failed to download chunk ${chunk.chunk_index} after ${this.options.retries} attempts: ${lastError?.message}`);
|
|
94
101
|
}
|
|
95
102
|
/**
|
|
96
103
|
* Concatenate multiple Uint8Array buffers into a single buffer.
|
|
97
104
|
* Pre-allocates the result array for efficiency.
|
|
98
105
|
*/
|
|
99
106
|
concatenateBuffers(buffers) {
|
|
100
|
-
if (buffers.length === 0) throw
|
|
107
|
+
if (buffers.length === 0) throw ValidationError.missingField("buffers");
|
|
101
108
|
if (buffers.length === 1) return buffers[0];
|
|
102
109
|
const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);
|
|
103
110
|
const result = new Uint8Array(totalLength);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arrow-stream-processor.js","names":[
|
|
1
|
+
{"version":3,"file":"arrow-stream-processor.js","names":[],"sources":["../../src/stream/arrow-stream-processor.ts"],"sourcesContent":["import type { sql } from \"@databricks/sdk-experimental\";\nimport { ExecutionError, ValidationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"stream:arrow\");\n\ntype ResultManifest = sql.ResultManifest;\ntype ExternalLink = sql.ExternalLink;\n\nexport interface ArrowStreamOptions {\n maxConcurrentDownloads: number;\n timeout: number;\n retries: number;\n}\n\n/**\n * Result from zero-copy Arrow chunk processing.\n * Contains raw IPC bytes without server-side parsing.\n */\nexport interface ArrowRawResult {\n /** Concatenated raw Arrow IPC bytes */\n data: Uint8Array;\n /** Schema from Databricks manifest (not parsed from Arrow) */\n schema: ResultManifest[\"schema\"];\n}\n\nconst BACKOFF_MULTIPLIER = 1000;\n\nexport class ArrowStreamProcessor {\n static readonly DEFAULT_MAX_CONCURRENT_DOWNLOADS = 5;\n static readonly DEFAULT_TIMEOUT = 30000;\n static readonly DEFAULT_RETRIES = 3;\n\n constructor(\n private options: ArrowStreamOptions = {\n maxConcurrentDownloads:\n ArrowStreamProcessor.DEFAULT_MAX_CONCURRENT_DOWNLOADS,\n timeout: ArrowStreamProcessor.DEFAULT_TIMEOUT,\n retries: ArrowStreamProcessor.DEFAULT_RETRIES,\n },\n ) {\n this.options = {\n maxConcurrentDownloads:\n options.maxConcurrentDownloads ??\n ArrowStreamProcessor.DEFAULT_MAX_CONCURRENT_DOWNLOADS,\n timeout: options.timeout ?? ArrowStreamProcessor.DEFAULT_TIMEOUT,\n retries: options.retries ?? ArrowStreamProcessor.DEFAULT_RETRIES,\n };\n }\n\n /**\n * Process Arrow chunks using zero-copy proxy pattern.\n *\n * Downloads raw IPC bytes from external links and concatenates them\n * without parsing into Arrow Tables on the server. This reduces:\n * - Memory usage by ~50% (no parsed Table representation)\n * - CPU usage (no tableFromIPC/tableToIPC calls)\n *\n * The client is responsible for parsing the IPC bytes.\n *\n * @param chunks - External links to Arrow IPC data\n * @param schema - Schema from Databricks manifest\n * @param signal - Optional abort signal\n * @returns Raw concatenated IPC bytes with schema\n */\n async processChunks(\n chunks: ExternalLink[],\n schema: ResultManifest[\"schema\"],\n signal?: AbortSignal,\n ): Promise<ArrowRawResult> {\n if (chunks.length === 0) {\n throw ValidationError.missingField(\"chunks\");\n }\n\n const buffers = await this.downloadChunksRaw(chunks, signal);\n const data = this.concatenateBuffers(buffers);\n\n return { data, schema };\n }\n\n /**\n * Download all chunks as raw bytes with concurrency control.\n */\n private async downloadChunksRaw(\n chunks: ExternalLink[],\n signal?: AbortSignal,\n ): Promise<Uint8Array[]> {\n const semaphore = new Semaphore(this.options.maxConcurrentDownloads);\n\n const downloadPromises = chunks.map(async (chunk) => {\n await semaphore.acquire();\n try {\n return await this.downloadChunkRaw(chunk, signal);\n } finally {\n semaphore.release();\n }\n });\n\n return Promise.all(downloadPromises);\n }\n\n /**\n * Download a single chunk as raw bytes with retry logic.\n */\n private async downloadChunkRaw(\n chunk: ExternalLink,\n signal?: AbortSignal,\n ): Promise<Uint8Array> {\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt < this.options.retries; attempt++) {\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => {\n timeoutController.abort();\n }, this.options.timeout);\n\n const combinedSignal = signal\n ? this.combineAbortSignals(signal, timeoutController.signal)\n : timeoutController.signal;\n\n try {\n const externalLink = chunk.external_link;\n if (!externalLink) {\n logger.error(\"External link is required for chunk: %O\", chunk);\n continue;\n }\n\n const response = await fetch(externalLink, {\n signal: combinedSignal,\n });\n\n if (!response.ok) {\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index}: ${response.status} ${response.statusText}`,\n );\n }\n\n const arrayBuffer = await response.arrayBuffer();\n return new Uint8Array(arrayBuffer);\n } catch (error) {\n lastError = error as Error;\n\n if (timeoutController.signal.aborted) {\n lastError = new Error(\n `Chunk ${chunk.chunk_index} download timed out after ${this.options.timeout}ms`,\n );\n }\n\n if (signal?.aborted) {\n throw ExecutionError.canceled();\n }\n\n if (attempt < this.options.retries - 1) {\n await this.delay(2 ** attempt * BACKOFF_MULTIPLIER);\n }\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index} after ${this.options.retries} attempts: ${lastError?.message}`,\n );\n }\n\n /**\n * Concatenate multiple Uint8Array buffers into a single buffer.\n * Pre-allocates the result array for efficiency.\n */\n private concatenateBuffers(buffers: Uint8Array[]): Uint8Array {\n if (buffers.length === 0) {\n throw ValidationError.missingField(\"buffers\");\n }\n\n if (buffers.length === 1) {\n return buffers[0];\n }\n\n const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);\n const result = new Uint8Array(totalLength);\n\n let offset = 0;\n for (const buffer of buffers) {\n result.set(buffer, offset);\n offset += buffer.length;\n }\n\n return result;\n }\n\n /**\n * Combines multiple AbortSignals into one.\n * The combined signal aborts when any of the input signals abort.\n */\n private combineAbortSignals(...signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort();\n return controller.signal;\n }\n signal.addEventListener(\"abort\", () => controller.abort(), {\n once: true,\n });\n }\n\n return controller.signal;\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n\nclass Semaphore {\n private permits: number;\n private waiting: (() => void)[] = [];\n\n constructor(permits: number) {\n this.permits = permits;\n }\n\n async acquire(): Promise<void> {\n if (this.permits > 0) {\n this.permits--;\n return;\n }\n\n return new Promise<void>((resolve) => {\n this.waiting.push(resolve);\n });\n }\n\n release(): void {\n if (this.waiting.length > 0) {\n const next = this.waiting.shift();\n\n if (next) {\n next();\n }\n } else {\n this.permits++;\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/stream/buffers.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { ValidationError } from "../errors/validation.js";
|
|
2
|
+
import { init_errors } from "../errors/index.js";
|
|
3
|
+
|
|
1
4
|
//#region src/stream/buffers.ts
|
|
5
|
+
init_errors();
|
|
2
6
|
var RingBuffer = class {
|
|
3
7
|
constructor(capacity, keyExtractor) {
|
|
4
|
-
if (capacity <= 0) throw
|
|
8
|
+
if (capacity <= 0) throw ValidationError.invalidValue("capacity", capacity, "greater than 0");
|
|
5
9
|
this.capacity = capacity;
|
|
6
10
|
this.buffer = new Array(capacity).fill(null);
|
|
7
11
|
this.writeIndex = 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buffers.js","names":[
|
|
1
|
+
{"version":3,"file":"buffers.js","names":[],"sources":["../../src/stream/buffers.ts"],"sourcesContent":["import { ValidationError } from \"../errors\";\nimport type { BufferedEvent } from \"./types\";\n\n// generic ring buffer implementation\nexport class RingBuffer<T> {\n public buffer: (T | null)[];\n public capacity: number;\n private writeIndex: number;\n private size: number;\n private keyExtractor: (item: T) => string;\n private keyIndex: Map<string, number>;\n\n constructor(capacity: number, keyExtractor: (item: T) => string) {\n if (capacity <= 0) {\n throw ValidationError.invalidValue(\n \"capacity\",\n capacity,\n \"greater than 0\",\n );\n }\n\n this.capacity = capacity;\n this.buffer = new Array(capacity).fill(null);\n this.writeIndex = 0;\n this.size = 0;\n this.keyExtractor = keyExtractor;\n this.keyIndex = new Map();\n }\n\n // add an item to the buffer\n add(item: T): void {\n const key = this.keyExtractor(item);\n\n // check if item already exists\n const existingIndex = this.keyIndex.get(key);\n if (existingIndex !== undefined) {\n // update existing item\n this.buffer[existingIndex] = item;\n return;\n }\n\n // evict least recently used item if at capacity\n const evicted = this.buffer[this.writeIndex];\n if (evicted !== null) {\n const evictedKey = this.keyExtractor(evicted);\n this.keyIndex.delete(evictedKey);\n }\n\n // add new item\n this.buffer[this.writeIndex] = item;\n this.keyIndex.set(key, this.writeIndex);\n\n // update write index and size\n this.writeIndex = (this.writeIndex + 1) % this.capacity;\n this.size = Math.min(this.size + 1, this.capacity);\n }\n\n // get an item from the buffer\n get(key: string): T | null {\n const index = this.keyIndex.get(key);\n if (index === undefined) return null;\n\n return this.buffer[index];\n }\n\n // check if an item exists in the buffer\n has(key: string): boolean {\n return this.keyIndex.has(key);\n }\n\n // remove an item from the buffer\n remove(key: string): void {\n const index = this.keyIndex.get(key);\n if (index === undefined) return;\n\n // remove item from buffer\n this.buffer[index] = null;\n this.keyIndex.delete(key);\n\n // update size\n this.size = Math.max(this.size - 1, 0);\n }\n\n // get all items from the buffer\n getAll(): T[] {\n const result: T[] = [];\n\n // iterate over buffer in order of insertion\n for (let i = 0; i < this.size; i++) {\n // calculate index of item in buffer\n const index =\n (this.writeIndex - this.size + i + this.capacity) % this.capacity;\n // add item to result if not null\n const item = this.buffer[index];\n if (item !== null) {\n result.push(item);\n }\n }\n return result;\n }\n\n // get the size of the buffer\n getSize(): number {\n return this.size;\n }\n\n // clear the buffer\n clear(): void {\n this.buffer = new Array(this.capacity).fill(null);\n this.keyIndex.clear();\n this.writeIndex = 0;\n this.size = 0;\n }\n}\n\n// event ring buffer implementation\nexport class EventRingBuffer {\n private buffer: RingBuffer<BufferedEvent>;\n\n constructor(capacity: number = 100) {\n this.buffer = new RingBuffer<BufferedEvent>(capacity, (event) => event.id);\n }\n\n // add an event to the buffer\n add(event: BufferedEvent): void {\n this.buffer.add(event);\n }\n\n // check if an event exists in the buffer\n has(eventId: string): boolean {\n return this.buffer.has(eventId);\n }\n\n // get all events since a given event id\n getEventsSince(lastEventId: string): BufferedEvent[] {\n const allEvents = this.buffer.getAll();\n const result: BufferedEvent[] = [];\n // flag to track if we've found the last event\n let foundLastEvent = false;\n\n // iterate over all events\n for (const event of allEvents) {\n // if found, add to result\n if (foundLastEvent) {\n result.push(event);\n // if not found, check if it's the last event\n } else if (event.id === lastEventId) {\n foundLastEvent = true;\n }\n }\n return result;\n }\n\n clear(): void {\n this.buffer.clear();\n }\n}\n"],"mappings":";;;;aAA4C;AAI5C,IAAa,aAAb,MAA2B;CAQzB,YAAY,UAAkB,cAAmC;AAC/D,MAAI,YAAY,EACd,OAAM,gBAAgB,aACpB,YACA,UACA,iBACD;AAGH,OAAK,WAAW;AAChB,OAAK,SAAS,IAAI,MAAM,SAAS,CAAC,KAAK,KAAK;AAC5C,OAAK,aAAa;AAClB,OAAK,OAAO;AACZ,OAAK,eAAe;AACpB,OAAK,2BAAW,IAAI,KAAK;;CAI3B,IAAI,MAAe;EACjB,MAAM,MAAM,KAAK,aAAa,KAAK;EAGnC,MAAM,gBAAgB,KAAK,SAAS,IAAI,IAAI;AAC5C,MAAI,kBAAkB,QAAW;AAE/B,QAAK,OAAO,iBAAiB;AAC7B;;EAIF,MAAM,UAAU,KAAK,OAAO,KAAK;AACjC,MAAI,YAAY,MAAM;GACpB,MAAM,aAAa,KAAK,aAAa,QAAQ;AAC7C,QAAK,SAAS,OAAO,WAAW;;AAIlC,OAAK,OAAO,KAAK,cAAc;AAC/B,OAAK,SAAS,IAAI,KAAK,KAAK,WAAW;AAGvC,OAAK,cAAc,KAAK,aAAa,KAAK,KAAK;AAC/C,OAAK,OAAO,KAAK,IAAI,KAAK,OAAO,GAAG,KAAK,SAAS;;CAIpD,IAAI,KAAuB;EACzB,MAAM,QAAQ,KAAK,SAAS,IAAI,IAAI;AACpC,MAAI,UAAU,OAAW,QAAO;AAEhC,SAAO,KAAK,OAAO;;CAIrB,IAAI,KAAsB;AACxB,SAAO,KAAK,SAAS,IAAI,IAAI;;CAI/B,OAAO,KAAmB;EACxB,MAAM,QAAQ,KAAK,SAAS,IAAI,IAAI;AACpC,MAAI,UAAU,OAAW;AAGzB,OAAK,OAAO,SAAS;AACrB,OAAK,SAAS,OAAO,IAAI;AAGzB,OAAK,OAAO,KAAK,IAAI,KAAK,OAAO,GAAG,EAAE;;CAIxC,SAAc;EACZ,MAAM,SAAc,EAAE;AAGtB,OAAK,IAAI,IAAI,GAAG,IAAI,KAAK,MAAM,KAAK;GAElC,MAAM,SACH,KAAK,aAAa,KAAK,OAAO,IAAI,KAAK,YAAY,KAAK;GAE3D,MAAM,OAAO,KAAK,OAAO;AACzB,OAAI,SAAS,KACX,QAAO,KAAK,KAAK;;AAGrB,SAAO;;CAIT,UAAkB;AAChB,SAAO,KAAK;;CAId,QAAc;AACZ,OAAK,SAAS,IAAI,MAAM,KAAK,SAAS,CAAC,KAAK,KAAK;AACjD,OAAK,SAAS,OAAO;AACrB,OAAK,aAAa;AAClB,OAAK,OAAO;;;AAKhB,IAAa,kBAAb,MAA6B;CAG3B,YAAY,WAAmB,KAAK;AAClC,OAAK,SAAS,IAAI,WAA0B,WAAW,UAAU,MAAM,GAAG;;CAI5E,IAAI,OAA4B;AAC9B,OAAK,OAAO,IAAI,MAAM;;CAIxB,IAAI,SAA0B;AAC5B,SAAO,KAAK,OAAO,IAAI,QAAQ;;CAIjC,eAAe,aAAsC;EACnD,MAAM,YAAY,KAAK,OAAO,QAAQ;EACtC,MAAM,SAA0B,EAAE;EAElC,IAAI,iBAAiB;AAGrB,OAAK,MAAM,SAAS,UAElB,KAAI,eACF,QAAO,KAAK,MAAM;WAET,MAAM,OAAO,YACtB,kBAAiB;AAGrB,SAAO;;CAGT,QAAc;AACZ,OAAK,OAAO,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-writer.js","names":[
|
|
1
|
+
{"version":3,"file":"sse-writer.js","names":[],"sources":["../../src/stream/sse-writer.ts"],"sourcesContent":["import type { IAppResponse } from \"shared\";\nimport { streamDefaults } from \"./defaults\";\nimport {\n type BufferedEvent,\n type SSEError,\n SSEErrorCode,\n SSEWarningCode,\n} from \"./types\";\nimport { StreamValidator } from \"./validator\";\n\nexport class SSEWriter {\n // setup SSE headers\n setupHeaders(res: IAppResponse): void {\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n res.setHeader(\"Content-Encoding\", \"none\");\n\n res.flushHeaders?.();\n }\n\n // write a single event to the response\n writeEvent(res: IAppResponse, eventId: string, event: any): void {\n if (res.writableEnded) return;\n\n const eventType = StreamValidator.sanitizeEventType(event.type);\n const eventData = JSON.stringify(event);\n\n res.write(`id: ${eventId}\\n`);\n res.write(`event: ${eventType}\\n`);\n res.write(`data: ${eventData}\\n\\n`);\n }\n writeError(\n res: IAppResponse,\n eventId: string,\n error: string,\n code: SSEErrorCode = SSEErrorCode.INTERNAL_ERROR,\n ): void {\n if (res.writableEnded) return;\n\n const errorData: SSEError = {\n error,\n code,\n };\n\n res.write(`id: ${eventId}\\n`);\n res.write(`event: error\\n`);\n res.write(`data: ${JSON.stringify(errorData)}\\n\\n`);\n }\n\n // write a buffered event for replay\n writeBufferedEvent(res: IAppResponse, event: BufferedEvent): void {\n if (res.writableEnded) return;\n\n res.write(`id: ${event.id}\\n`);\n res.write(`event: ${event.type}\\n`);\n res.write(`data: ${event.data}\\n\\n`);\n }\n\n // write a buffer overflow warning\n writeBufferOverflowWarning(res: IAppResponse, lastEventId: string): void {\n if (res.writableEnded) return;\n\n try {\n res.write(`event: warning\\n`);\n res.write(\n `data: ${JSON.stringify({\n warning: \"Buffer overflow detected - some events were lost\",\n code: SSEWarningCode.BUFFER_OVERFLOW_RESTART,\n lastEventId,\n })}\\n\\n`,\n );\n } catch (_error) {\n // ignore write errors - client will ignore this event\n }\n }\n\n // start the heartbeat interval\n startHeartbeat(\n res: IAppResponse,\n signal: AbortSignal,\n interval?: number,\n ): NodeJS.Timeout {\n const heartbeatInterval = interval ?? streamDefaults.heartbeatInterval;\n\n return setInterval(() => {\n if (!signal.aborted && !res.writableEnded) {\n try {\n res.write(`: heartbeat\\n\\n`);\n } catch (_error) {\n // ignore write errors - client will ignore this event\n }\n }\n }, heartbeatInterval);\n }\n}\n"],"mappings":";;;;;AAUA,IAAa,YAAb,MAAuB;CAErB,aAAa,KAAyB;AACpC,MAAI,UAAU,gBAAgB,oBAAoB;AAClD,MAAI,UAAU,iBAAiB,WAAW;AAC1C,MAAI,UAAU,cAAc,aAAa;AACzC,MAAI,UAAU,oBAAoB,OAAO;AAEzC,MAAI,gBAAgB;;CAItB,WAAW,KAAmB,SAAiB,OAAkB;AAC/D,MAAI,IAAI,cAAe;EAEvB,MAAM,YAAY,gBAAgB,kBAAkB,MAAM,KAAK;EAC/D,MAAM,YAAY,KAAK,UAAU,MAAM;AAEvC,MAAI,MAAM,OAAO,QAAQ,IAAI;AAC7B,MAAI,MAAM,UAAU,UAAU,IAAI;AAClC,MAAI,MAAM,SAAS,UAAU,MAAM;;CAErC,WACE,KACA,SACA,OACA,OAAqB,aAAa,gBAC5B;AACN,MAAI,IAAI,cAAe;EAEvB,MAAM,YAAsB;GAC1B;GACA;GACD;AAED,MAAI,MAAM,OAAO,QAAQ,IAAI;AAC7B,MAAI,MAAM,iBAAiB;AAC3B,MAAI,MAAM,SAAS,KAAK,UAAU,UAAU,CAAC,MAAM;;CAIrD,mBAAmB,KAAmB,OAA4B;AAChE,MAAI,IAAI,cAAe;AAEvB,MAAI,MAAM,OAAO,MAAM,GAAG,IAAI;AAC9B,MAAI,MAAM,UAAU,MAAM,KAAK,IAAI;AACnC,MAAI,MAAM,SAAS,MAAM,KAAK,MAAM;;CAItC,2BAA2B,KAAmB,aAA2B;AACvE,MAAI,IAAI,cAAe;AAEvB,MAAI;AACF,OAAI,MAAM,mBAAmB;AAC7B,OAAI,MACF,SAAS,KAAK,UAAU;IACtB,SAAS;IACT,MAAM,eAAe;IACrB;IACD,CAAC,CAAC,MACJ;WACM,QAAQ;;CAMnB,eACE,KACA,QACA,UACgB;EAChB,MAAM,oBAAoB,YAAY,eAAe;AAErD,SAAO,kBAAkB;AACvB,OAAI,CAAC,OAAO,WAAW,CAAC,IAAI,cAC1B,KAAI;AACF,QAAI,MAAM,kBAAkB;YACrB,QAAQ;KAIlB,kBAAkB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-manager.d.ts","names":[],"sources":["../../src/stream/stream-manager.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"stream-manager.d.ts","names":[],"sources":["../../src/stream/stream-manager.ts"],"sourcesContent":[],"mappings":";;;;cAWa,aAAA;;;EAAA,QAAA,SAAa;EAAA,QAAA,YAAA;UAOF,SAAA;aAYf,CAAA,OAAA,CAAA,EAZe,YAYf;QACa,CAAA,GAAA,EADb,YACa,EAAA,OAAA,EAAA,CAAA,MAAA,EAAA,WAAA,EAAA,GAAgB,cAAhB,CAAA,GAAA,EAAA,IAAA,EAAA,OAAA,CAAA,EAAA,OAAA,CAAA,EACR,YADQ,CAAA,EAEjB,OAFiB,CAAA,IAAA,CAAA;UAAgB,CAAA,CAAA,EAAA,IAAA;gBACxB,CAAA,CAAA,EAAA,MAAA;UACT,uBAAA;EAAO,QAAA,gBAAA"}
|
|
@@ -5,6 +5,7 @@ import { StreamValidator } from "./validator.js";
|
|
|
5
5
|
import { SSEWriter } from "./sse-writer.js";
|
|
6
6
|
import { StreamRegistry } from "./stream-registry.js";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { context } from "@opentelemetry/api";
|
|
8
9
|
|
|
9
10
|
//#region src/stream/stream-manager.ts
|
|
10
11
|
var StreamManager = class {
|
|
@@ -15,8 +16,9 @@ var StreamManager = class {
|
|
|
15
16
|
this.bufferTTL = options?.bufferTTL ?? streamDefaults.bufferTTL;
|
|
16
17
|
this.activeOperations = /* @__PURE__ */ new Set();
|
|
17
18
|
}
|
|
18
|
-
stream(res, handler, options) {
|
|
19
|
+
async stream(res, handler, options) {
|
|
19
20
|
const { streamId } = options || {};
|
|
21
|
+
if (res.writableEnded || res.destroyed) return;
|
|
20
22
|
this.sseWriter.setupHeaders(res);
|
|
21
23
|
if (streamId && StreamValidator.validateStreamId(streamId)) {
|
|
22
24
|
const existingStream = this.streamRegistry.get(streamId);
|
|
@@ -73,10 +75,16 @@ var StreamManager = class {
|
|
|
73
75
|
}
|
|
74
76
|
async _createNewStream(res, handler, options) {
|
|
75
77
|
const streamId = options?.streamId ?? randomUUID();
|
|
78
|
+
if (res.writableEnded || res.destroyed) return;
|
|
76
79
|
const abortController = new AbortController();
|
|
77
80
|
const eventBuffer = new EventRingBuffer(options?.bufferSize ?? streamDefaults.bufferSize);
|
|
78
81
|
const combinedSignal = this._combineSignals(abortController.signal, options?.userSignal);
|
|
79
82
|
const heartbeat = this.sseWriter.startHeartbeat(res, combinedSignal);
|
|
83
|
+
const traceContext = context.active();
|
|
84
|
+
if (res.writableEnded || res.destroyed) {
|
|
85
|
+
clearInterval(heartbeat);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
80
88
|
const streamEntry = {
|
|
81
89
|
streamId,
|
|
82
90
|
generator: handler(combinedSignal),
|
|
@@ -84,7 +92,8 @@ var StreamManager = class {
|
|
|
84
92
|
clients: new Set([res]),
|
|
85
93
|
isCompleted: false,
|
|
86
94
|
lastAccess: Date.now(),
|
|
87
|
-
abortController
|
|
95
|
+
abortController,
|
|
96
|
+
traceContext
|
|
88
97
|
};
|
|
89
98
|
this.streamRegistry.add(streamEntry);
|
|
90
99
|
const streamOperation = {
|
|
@@ -103,45 +112,47 @@ var StreamManager = class {
|
|
|
103
112
|
this.activeOperations.delete(streamOperation);
|
|
104
113
|
}
|
|
105
114
|
async _processGeneratorInBackground(streamEntry) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
return context.with(streamEntry.traceContext, async () => {
|
|
116
|
+
try {
|
|
117
|
+
for await (const event of streamEntry.generator) {
|
|
118
|
+
if (streamEntry.abortController.signal.aborted) break;
|
|
119
|
+
const eventId = randomUUID();
|
|
120
|
+
const eventData = JSON.stringify(event);
|
|
121
|
+
if (eventData.length > this.maxEventSize) {
|
|
122
|
+
const errorMsg = `Event exceeds max size of ${this.maxEventSize} bytes`;
|
|
123
|
+
const errorCode = SSEErrorCode.INVALID_REQUEST;
|
|
124
|
+
this._broadcastErrorToClients(streamEntry, eventId, errorMsg, errorCode);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
streamEntry.eventBuffer.add({
|
|
128
|
+
id: eventId,
|
|
129
|
+
type: event.type,
|
|
130
|
+
data: eventData,
|
|
131
|
+
timestamp: Date.now()
|
|
132
|
+
});
|
|
133
|
+
this._broadcastEventsToClients(streamEntry, eventId, event);
|
|
134
|
+
streamEntry.lastAccess = Date.now();
|
|
116
135
|
}
|
|
136
|
+
streamEntry.isCompleted = true;
|
|
137
|
+
this._closeAllClients(streamEntry);
|
|
138
|
+
this._cleanupStream(streamEntry);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const errorMsg = error instanceof Error ? error.message : "Internal server error";
|
|
141
|
+
const errorEventId = randomUUID();
|
|
142
|
+
const errorCode = this._categorizeError(error);
|
|
117
143
|
streamEntry.eventBuffer.add({
|
|
118
|
-
id:
|
|
119
|
-
type:
|
|
120
|
-
data:
|
|
144
|
+
id: errorEventId,
|
|
145
|
+
type: "error",
|
|
146
|
+
data: JSON.stringify({
|
|
147
|
+
error: errorMsg,
|
|
148
|
+
code: errorCode
|
|
149
|
+
}),
|
|
121
150
|
timestamp: Date.now()
|
|
122
151
|
});
|
|
123
|
-
this.
|
|
124
|
-
streamEntry.
|
|
152
|
+
this._broadcastErrorToClients(streamEntry, errorEventId, errorMsg, errorCode, true);
|
|
153
|
+
streamEntry.isCompleted = true;
|
|
125
154
|
}
|
|
126
|
-
|
|
127
|
-
this._closeAllClients(streamEntry);
|
|
128
|
-
this._cleanupStream(streamEntry);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
const errorMsg = error instanceof Error ? error.message : "Internal server error";
|
|
131
|
-
const errorEventId = randomUUID();
|
|
132
|
-
const errorCode = this._categorizeError(error);
|
|
133
|
-
streamEntry.eventBuffer.add({
|
|
134
|
-
id: errorEventId,
|
|
135
|
-
type: "error",
|
|
136
|
-
data: JSON.stringify({
|
|
137
|
-
error: errorMsg,
|
|
138
|
-
code: errorCode
|
|
139
|
-
}),
|
|
140
|
-
timestamp: Date.now()
|
|
141
|
-
});
|
|
142
|
-
this._broadcastErrorToClients(streamEntry, errorEventId, errorMsg, errorCode, true);
|
|
143
|
-
streamEntry.isCompleted = true;
|
|
144
|
-
}
|
|
155
|
+
});
|
|
145
156
|
}
|
|
146
157
|
_combineSignals(internalSignal, userSignal) {
|
|
147
158
|
if (!userSignal) return internalSignal || new AbortController().signal;
|