@databricks/appkit 0.1.5 → 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 +52 -0
- package/CLAUDE.md +52 -0
- package/NOTICE.md +2 -0
- package/README.md +21 -15
- package/bin/appkit-lint.js +129 -0
- package/dist/analytics/analytics.d.ts.map +1 -1
- package/dist/analytics/analytics.js +16 -3
- package/dist/analytics/analytics.js.map +1 -1
- package/dist/analytics/query.js +8 -2
- 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/index.d.ts.map +1 -1
- package/dist/cache/index.js +24 -3
- package/dist/cache/index.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 +25 -14
- package/dist/connectors/lakebase/client.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/context/service-context.js +13 -8
- package/dist/context/service-context.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 -3
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -0
- 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 +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +9 -4
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +22 -17
- 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/vite-dev-server.js +8 -3
- package/dist/server/vite-dev-server.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/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/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/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 -1
- package/dist/utils/env-validator.js.map +1 -1
- package/dist/utils/path-exclusions.js +66 -0
- package/dist/utils/path-exclusions.js.map +1 -0
- package/llms.txt +52 -0
- package/package.json +4 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//#region src/logging/wide-event.ts
|
|
2
|
+
/**
|
|
3
|
+
* WideEvent
|
|
4
|
+
* - Represents a single event for a request
|
|
5
|
+
* - Fields are camelCase to match OpenTelemetry
|
|
6
|
+
*/
|
|
7
|
+
var WideEvent = class {
|
|
8
|
+
constructor(requestId) {
|
|
9
|
+
this.startTime = Date.now();
|
|
10
|
+
this.data = {
|
|
11
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12
|
+
request_id: requestId,
|
|
13
|
+
service: {
|
|
14
|
+
name: "appkit",
|
|
15
|
+
version: process.env.npm_package_version || "unknown",
|
|
16
|
+
region: process.env.REGION,
|
|
17
|
+
deployment_id: process.env.DEPLOYMENT_ID,
|
|
18
|
+
node_env: process.env.NODE_ENV
|
|
19
|
+
},
|
|
20
|
+
logs: [],
|
|
21
|
+
context: {}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Set a value in the event
|
|
26
|
+
* @param key - The key to set
|
|
27
|
+
* @param value - The value to set
|
|
28
|
+
* @returns The event
|
|
29
|
+
*/
|
|
30
|
+
set(key, value) {
|
|
31
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) this.data[key] = {
|
|
32
|
+
...this.data[key],
|
|
33
|
+
...value
|
|
34
|
+
};
|
|
35
|
+
else this.data[key] = value;
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Set the component name and operation.
|
|
40
|
+
* Component can be a plugin, connector, or service.
|
|
41
|
+
* @param name - The name of the component (e.g., "analytics", "sql-warehouse", "cache-manager")
|
|
42
|
+
* @param operation - The operation being performed (e.g., "query", "getOrExecute")
|
|
43
|
+
* @returns The event
|
|
44
|
+
*/
|
|
45
|
+
setComponent(name, operation) {
|
|
46
|
+
this.data.component = {
|
|
47
|
+
name,
|
|
48
|
+
operation
|
|
49
|
+
};
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Set the user context
|
|
54
|
+
* @param user - The user context
|
|
55
|
+
* @returns The event
|
|
56
|
+
*/
|
|
57
|
+
setUser(user) {
|
|
58
|
+
this.data.user = {
|
|
59
|
+
...this.data.user,
|
|
60
|
+
...user
|
|
61
|
+
};
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Set the execution context
|
|
66
|
+
* @param execution - The execution context
|
|
67
|
+
* @returns The event
|
|
68
|
+
*/
|
|
69
|
+
setExecution(execution) {
|
|
70
|
+
this.data.execution = {
|
|
71
|
+
...this.data.execution,
|
|
72
|
+
...execution
|
|
73
|
+
};
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Set the stream context
|
|
78
|
+
* @param stream - The stream context
|
|
79
|
+
* @returns The event
|
|
80
|
+
*/
|
|
81
|
+
setStream(stream) {
|
|
82
|
+
this.data.stream = {
|
|
83
|
+
...this.data.stream,
|
|
84
|
+
...stream
|
|
85
|
+
};
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Set the error context
|
|
90
|
+
* @param error - The error context
|
|
91
|
+
* @returns The event
|
|
92
|
+
*/
|
|
93
|
+
setError(error) {
|
|
94
|
+
const isAppKitError = "code" in error && "statusCode" in error;
|
|
95
|
+
const errorCause = error.cause;
|
|
96
|
+
this.data.error = {
|
|
97
|
+
type: error.name,
|
|
98
|
+
code: isAppKitError ? error.code : "UNKNOWN_ERROR",
|
|
99
|
+
message: error.message,
|
|
100
|
+
retriable: isAppKitError ? error.isRetryable : false,
|
|
101
|
+
cause: errorCause ? String(errorCause) : void 0
|
|
102
|
+
};
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Add scoped context to the event
|
|
107
|
+
* @param scope - The scope name (plugin, connector, or service name)
|
|
108
|
+
* @param ctx - Context data to merge
|
|
109
|
+
* @example
|
|
110
|
+
* event.setContext("analytics", { query_key: "apps_list", rows_returned: 100 });
|
|
111
|
+
* event.setContext("sql-warehouse", { warehouse_id: "1234567890" });
|
|
112
|
+
*/
|
|
113
|
+
setContext(scope, ctx) {
|
|
114
|
+
if (!this.data.context) this.data.context = {};
|
|
115
|
+
this.data.context[scope] = {
|
|
116
|
+
...this.data.context[scope],
|
|
117
|
+
...ctx
|
|
118
|
+
};
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Add a log to the event
|
|
123
|
+
* @param level - The level of the log
|
|
124
|
+
* @param message - The message of the log
|
|
125
|
+
* @param context - The context of the log
|
|
126
|
+
* @returns The event
|
|
127
|
+
*/
|
|
128
|
+
addLog(level, message, context) {
|
|
129
|
+
if (!this.data.logs) this.data.logs = [];
|
|
130
|
+
this.data.logs.push({
|
|
131
|
+
level,
|
|
132
|
+
message,
|
|
133
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
134
|
+
context
|
|
135
|
+
});
|
|
136
|
+
if (this.data.logs.length > 50) this.data.logs = this.data.logs.slice(-50);
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Finalize the event
|
|
141
|
+
* @param statusCode - The status code of the response
|
|
142
|
+
* @returns The event data
|
|
143
|
+
*/
|
|
144
|
+
finalize(statusCode) {
|
|
145
|
+
this.data.status_code = statusCode;
|
|
146
|
+
this.data.duration_ms = this.getDurationMs();
|
|
147
|
+
return this.data;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get the duration of the event in milliseconds
|
|
151
|
+
* @returns The duration of the event in milliseconds
|
|
152
|
+
*/
|
|
153
|
+
getDurationMs() {
|
|
154
|
+
return this.data.duration_ms || Date.now() - this.startTime;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Convert the event to a JSON object
|
|
158
|
+
* @returns The event data as a JSON object
|
|
159
|
+
*/
|
|
160
|
+
toJSON() {
|
|
161
|
+
return this.data;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
export { WideEvent };
|
|
167
|
+
//# sourceMappingURL=wide-event.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wide-event.js","names":[],"sources":["../../src/logging/wide-event.ts"],"sourcesContent":["import type { LogLevel } from \"./types\";\n\n/**\n * WideEvent data interface\n * - Represents a single event for a request\n * - Fields are camelCase to match OpenTelemetry\n */\nexport interface WideEventData {\n // request metadata\n timestamp: string;\n request_id: string;\n trace_id?: string;\n method?: string;\n path?: string;\n status_code?: number;\n duration_ms?: number;\n\n // service metadata\n service?: {\n name: string;\n version: string;\n region?: string;\n deployment_id?: string;\n node_env?: string;\n };\n\n // component metadata (plugin, connector, or service)\n component?: {\n name: string;\n operation?: string;\n };\n\n // user metadata\n user?: {\n id?: string;\n [key: string]: unknown;\n };\n\n // execution metadata\n execution?: {\n cache_hit?: boolean;\n cache_key?: string;\n cache_deduplication?: boolean;\n retry_attempts?: number;\n timeout_ms?: number;\n [key: string]: unknown;\n };\n\n // stream metadata\n stream?: {\n stream_id?: string;\n events_sent?: number;\n buffer_size?: number;\n reconnections?: number;\n [key: string]: unknown;\n };\n\n // error metadata\n error?: {\n type: string;\n code: string;\n message: string;\n retriable?: boolean;\n cause?: string;\n };\n\n // log metadata\n logs?: Array<{\n level: LogLevel;\n message: string;\n timestamp: string;\n context?: Record<string, unknown>;\n }>;\n\n /**\n * Scoped context data\n * Each scope (plugin, connector, service) can add its own namespaced data here.\n * Example: { analytics: { query_key: \"...\"}, \"sql-warehouse\": { warehouse_id: \"...\"} }\n */\n context?: Record<string, Record<string, unknown>>;\n\n [key: string]: unknown;\n}\n\n/**\n * WideEvent\n * - Represents a single event for a request\n * - Fields are camelCase to match OpenTelemetry\n */\nexport class WideEvent {\n public data: WideEventData;\n private startTime: number;\n\n constructor(requestId: string) {\n this.startTime = Date.now();\n this.data = {\n timestamp: new Date().toISOString(),\n request_id: requestId,\n service: {\n name: \"appkit\",\n version: process.env.npm_package_version || \"unknown\",\n region: process.env.REGION,\n deployment_id: process.env.DEPLOYMENT_ID,\n node_env: process.env.NODE_ENV,\n },\n logs: [],\n context: {},\n };\n }\n\n /**\n * Set a value in the event\n * @param key - The key to set\n * @param value - The value to set\n * @returns The event\n */\n set<K extends keyof WideEventData>(key: K, value: WideEventData[K]): this {\n if (typeof value === \"object\" && value !== null && !Array.isArray(value)) {\n // merge objects\n this.data[key] = {\n ...(this.data[key] as object),\n ...value,\n } as WideEventData[K];\n } else {\n this.data[key] = value;\n }\n return this;\n }\n\n /**\n * Set the component name and operation.\n * Component can be a plugin, connector, or service.\n * @param name - The name of the component (e.g., \"analytics\", \"sql-warehouse\", \"cache-manager\")\n * @param operation - The operation being performed (e.g., \"query\", \"getOrExecute\")\n * @returns The event\n */\n setComponent(name: string, operation?: string): this {\n this.data.component = { name, operation };\n return this;\n }\n\n /**\n * Set the user context\n * @param user - The user context\n * @returns The event\n */\n setUser(user: WideEventData[\"user\"]): this {\n this.data.user = { ...this.data.user, ...user };\n return this;\n }\n\n /**\n * Set the execution context\n * @param execution - The execution context\n * @returns The event\n */\n setExecution(execution: WideEventData[\"execution\"]): this {\n this.data.execution = { ...this.data.execution, ...execution };\n return this;\n }\n\n /**\n * Set the stream context\n * @param stream - The stream context\n * @returns The event\n */\n setStream(stream: WideEventData[\"stream\"]): this {\n this.data.stream = { ...this.data.stream, ...stream };\n return this;\n }\n\n /**\n * Set the error context\n * @param error - The error context\n * @returns The event\n */\n setError(error: Error): this {\n const isAppKitError = \"code\" in error && \"statusCode\" in error;\n const errorCause = (error as any).cause;\n\n this.data.error = {\n type: error.name,\n code: isAppKitError ? (error as any).code : \"UNKNOWN_ERROR\",\n message: error.message,\n retriable: isAppKitError ? (error as any).isRetryable : false,\n cause: errorCause ? String(errorCause) : undefined,\n };\n\n return this;\n }\n\n /**\n * Add scoped context to the event\n * @param scope - The scope name (plugin, connector, or service name)\n * @param ctx - Context data to merge\n * @example\n * event.setContext(\"analytics\", { query_key: \"apps_list\", rows_returned: 100 });\n * event.setContext(\"sql-warehouse\", { warehouse_id: \"1234567890\" });\n */\n setContext(scope: string, ctx: Record<string, unknown>): this {\n if (!this.data.context) {\n this.data.context = {};\n }\n\n this.data.context[scope] = {\n ...this.data.context[scope],\n ...ctx,\n };\n\n return this;\n }\n\n /**\n * Add a log to the event\n * @param level - The level of the log\n * @param message - The message of the log\n * @param context - The context of the log\n * @returns The event\n */\n addLog(\n level: LogLevel,\n message: string,\n context?: Record<string, unknown>,\n ): this {\n if (!this.data.logs) {\n this.data.logs = [];\n }\n\n this.data.logs.push({\n level,\n message,\n timestamp: new Date().toISOString(),\n context,\n });\n\n // Keep only last 50 logs to prevent unbounded growth\n if (this.data.logs.length > 50) {\n this.data.logs = this.data.logs.slice(-50);\n }\n\n return this;\n }\n\n /**\n * Finalize the event\n * @param statusCode - The status code of the response\n * @returns The event data\n */\n finalize(statusCode: number): WideEventData {\n this.data.status_code = statusCode;\n this.data.duration_ms = this.getDurationMs();\n return this.data;\n }\n\n /**\n * Get the duration of the event in milliseconds\n * @returns The duration of the event in milliseconds\n */\n getDurationMs(): number {\n return this.data.duration_ms || Date.now() - this.startTime;\n }\n\n /**\n * Convert the event to a JSON object\n * @returns The event data as a JSON object\n */\n toJSON(): WideEventData {\n return this.data;\n }\n}\n"],"mappings":";;;;;;AAyFA,IAAa,YAAb,MAAuB;CAIrB,YAAY,WAAmB;AAC7B,OAAK,YAAY,KAAK,KAAK;AAC3B,OAAK,OAAO;GACV,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC,YAAY;GACZ,SAAS;IACP,MAAM;IACN,SAAS,QAAQ,IAAI,uBAAuB;IAC5C,QAAQ,QAAQ,IAAI;IACpB,eAAe,QAAQ,IAAI;IAC3B,UAAU,QAAQ,IAAI;IACvB;GACD,MAAM,EAAE;GACR,SAAS,EAAE;GACZ;;;;;;;;CASH,IAAmC,KAAQ,OAA+B;AACxE,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,MAAM,CAEtE,MAAK,KAAK,OAAO;GACf,GAAI,KAAK,KAAK;GACd,GAAG;GACJ;MAED,MAAK,KAAK,OAAO;AAEnB,SAAO;;;;;;;;;CAUT,aAAa,MAAc,WAA0B;AACnD,OAAK,KAAK,YAAY;GAAE;GAAM;GAAW;AACzC,SAAO;;;;;;;CAQT,QAAQ,MAAmC;AACzC,OAAK,KAAK,OAAO;GAAE,GAAG,KAAK,KAAK;GAAM,GAAG;GAAM;AAC/C,SAAO;;;;;;;CAQT,aAAa,WAA6C;AACxD,OAAK,KAAK,YAAY;GAAE,GAAG,KAAK,KAAK;GAAW,GAAG;GAAW;AAC9D,SAAO;;;;;;;CAQT,UAAU,QAAuC;AAC/C,OAAK,KAAK,SAAS;GAAE,GAAG,KAAK,KAAK;GAAQ,GAAG;GAAQ;AACrD,SAAO;;;;;;;CAQT,SAAS,OAAoB;EAC3B,MAAM,gBAAgB,UAAU,SAAS,gBAAgB;EACzD,MAAM,aAAc,MAAc;AAElC,OAAK,KAAK,QAAQ;GAChB,MAAM,MAAM;GACZ,MAAM,gBAAiB,MAAc,OAAO;GAC5C,SAAS,MAAM;GACf,WAAW,gBAAiB,MAAc,cAAc;GACxD,OAAO,aAAa,OAAO,WAAW,GAAG;GAC1C;AAED,SAAO;;;;;;;;;;CAWT,WAAW,OAAe,KAAoC;AAC5D,MAAI,CAAC,KAAK,KAAK,QACb,MAAK,KAAK,UAAU,EAAE;AAGxB,OAAK,KAAK,QAAQ,SAAS;GACzB,GAAG,KAAK,KAAK,QAAQ;GACrB,GAAG;GACJ;AAED,SAAO;;;;;;;;;CAUT,OACE,OACA,SACA,SACM;AACN,MAAI,CAAC,KAAK,KAAK,KACb,MAAK,KAAK,OAAO,EAAE;AAGrB,OAAK,KAAK,KAAK,KAAK;GAClB;GACA;GACA,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC;GACD,CAAC;AAGF,MAAI,KAAK,KAAK,KAAK,SAAS,GAC1B,MAAK,KAAK,OAAO,KAAK,KAAK,KAAK,MAAM,IAAI;AAG5C,SAAO;;;;;;;CAQT,SAAS,YAAmC;AAC1C,OAAK,KAAK,cAAc;AACxB,OAAK,KAAK,cAAc,KAAK,eAAe;AAC5C,SAAO,KAAK;;;;;;CAOd,gBAAwB;AACtB,SAAO,KAAK,KAAK,eAAe,KAAK,KAAK,GAAG,KAAK;;;;;;CAOpD,SAAwB;AACtB,SAAO,KAAK"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-reader.d.ts","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"dev-reader.d.ts","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":[],"mappings":";;;;KAQK,sBAAA,SAAsB,QAAA,CACF,YACpB;;;AAT0C;;AAOpB,cAQd,aAAA,CAPY;iBACpB,QAAA;EAAgB,QAAA,mBAAA;EAMR,QAAA,WAAa,CAAA;EAAA,OAAA,WAAA,CAAA,CAAA,EAMF,aANE;sBAMF,CAAA,MAAA,EA+BO,sBA/BP,CAAA,EAAA,IAAA;UA+BO,CAAA,QAAA,EAAA,MAAA,EAAA,GAAA,EAAsB,QAAA,CAM1B,OANI,CAAA,EAO1B,OAP0B,CAAA,MAAA,CAAA"}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { createLogger } from "../logging/logger.js";
|
|
2
|
+
import { TunnelError } from "../errors/tunnel.js";
|
|
3
|
+
import { init_errors } from "../errors/index.js";
|
|
1
4
|
import { isRemoteTunnelAllowedByEnv } from "../server/remote-tunnel/gate.js";
|
|
2
5
|
import { randomUUID } from "node:crypto";
|
|
3
6
|
|
|
4
7
|
//#region src/plugin/dev-reader.ts
|
|
8
|
+
init_errors();
|
|
9
|
+
const logger = createLogger("plugin:dev-reader");
|
|
5
10
|
/**
|
|
6
11
|
* This class is used to read files from the local filesystem in dev mode
|
|
7
12
|
* through the WebSocket tunnel.
|
|
@@ -19,7 +24,7 @@ var DevFileReader = class DevFileReader {
|
|
|
19
24
|
if (isRemoteTunnelAllowedByEnv()) return Reflect.get(target, prop, receiver);
|
|
20
25
|
const value = Reflect.get(target, prop, receiver);
|
|
21
26
|
if (typeof value === "function") return function noop() {
|
|
22
|
-
|
|
27
|
+
logger.debug("Noop: %s (remote server disabled)", String(prop));
|
|
23
28
|
return Promise.resolve("");
|
|
24
29
|
};
|
|
25
30
|
return value;
|
|
@@ -34,9 +39,9 @@ var DevFileReader = class DevFileReader {
|
|
|
34
39
|
this.getTunnelForRequest = getter;
|
|
35
40
|
}
|
|
36
41
|
async readFile(filePath, req) {
|
|
37
|
-
if (!this.getTunnelForRequest) throw
|
|
42
|
+
if (!this.getTunnelForRequest) throw TunnelError.getterNotRegistered();
|
|
38
43
|
const tunnel = this.getTunnelForRequest(req);
|
|
39
|
-
if (!tunnel) throw
|
|
44
|
+
if (!tunnel) throw TunnelError.noConnection();
|
|
40
45
|
const { ws, pendingFileReads } = tunnel;
|
|
41
46
|
const requestId = randomUUID();
|
|
42
47
|
return new Promise((resolve, reject) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-reader.js","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { TunnelConnection } from \"shared\";\nimport { isRemoteTunnelAllowedByEnv } from \"@/server/remote-tunnel/gate\";\n\ntype TunnelConnectionGetter = (\n req: import(\"express\").Request,\n) => TunnelConnection | null;\n\n/**\n * This class is used to read files from the local filesystem in dev mode\n * through the WebSocket tunnel.\n */\nexport class DevFileReader {\n private static instance: DevFileReader | null = null;\n private getTunnelForRequest: TunnelConnectionGetter | null = null;\n\n private constructor() {}\n\n static getInstance(): DevFileReader {\n if (!DevFileReader.instance) {\n DevFileReader.instance = new Proxy(new DevFileReader(), {\n /**\n * We proxy the reader to return a noop function if the remote server is disabled.\n */\n get(target, prop, receiver) {\n if (isRemoteTunnelAllowedByEnv()) {\n return Reflect.get(target, prop, receiver);\n }\n\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value === \"function\") {\n return function noop() {\n
|
|
1
|
+
{"version":3,"file":"dev-reader.js","names":[],"sources":["../../src/plugin/dev-reader.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { TunnelConnection } from \"shared\";\nimport { isRemoteTunnelAllowedByEnv } from \"@/server/remote-tunnel/gate\";\nimport { TunnelError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"plugin:dev-reader\");\n\ntype TunnelConnectionGetter = (\n req: import(\"express\").Request,\n) => TunnelConnection | null;\n\n/**\n * This class is used to read files from the local filesystem in dev mode\n * through the WebSocket tunnel.\n */\nexport class DevFileReader {\n private static instance: DevFileReader | null = null;\n private getTunnelForRequest: TunnelConnectionGetter | null = null;\n\n private constructor() {}\n\n static getInstance(): DevFileReader {\n if (!DevFileReader.instance) {\n DevFileReader.instance = new Proxy(new DevFileReader(), {\n /**\n * We proxy the reader to return a noop function if the remote server is disabled.\n */\n get(target, prop, receiver) {\n if (isRemoteTunnelAllowedByEnv()) {\n return Reflect.get(target, prop, receiver);\n }\n\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value === \"function\") {\n return function noop() {\n logger.debug(\"Noop: %s (remote server disabled)\", String(prop));\n return Promise.resolve(\"\");\n };\n }\n\n return value;\n },\n set(target, prop, value, receiver) {\n return Reflect.set(target, prop, value, receiver);\n },\n });\n }\n\n return DevFileReader.instance;\n }\n\n registerTunnelGetter(getter: TunnelConnectionGetter) {\n this.getTunnelForRequest = getter;\n }\n\n async readFile(\n filePath: string,\n req: import(\"express\").Request,\n ): Promise<string> {\n if (!this.getTunnelForRequest) {\n throw TunnelError.getterNotRegistered();\n }\n const tunnel = this.getTunnelForRequest(req);\n\n if (!tunnel) {\n throw TunnelError.noConnection();\n }\n\n const { ws, pendingFileReads } = tunnel;\n const requestId = randomUUID();\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n pendingFileReads.delete(requestId);\n reject(new Error(`File read timeout: ${filePath}`));\n }, 10000);\n\n pendingFileReads.set(requestId, { resolve, reject, timeout });\n\n ws.send(\n JSON.stringify({\n type: \"file:read\",\n requestId,\n path: filePath,\n }),\n );\n });\n }\n}\n"],"mappings":";;;;;;;aAGwC;AAGxC,MAAM,SAAS,aAAa,oBAAoB;;;;;AAUhD,IAAa,gBAAb,MAAa,cAAc;;kBACuB;;CAGhD,AAAQ,cAAc;6BAFuC;;CAI7D,OAAO,cAA6B;AAClC,MAAI,CAAC,cAAc,SACjB,eAAc,WAAW,IAAI,MAAM,IAAI,eAAe,EAAE;GAItD,IAAI,QAAQ,MAAM,UAAU;AAC1B,QAAI,4BAA4B,CAC9B,QAAO,QAAQ,IAAI,QAAQ,MAAM,SAAS;IAG5C,MAAM,QAAQ,QAAQ,IAAI,QAAQ,MAAM,SAAS;AAEjD,QAAI,OAAO,UAAU,WACnB,QAAO,SAAS,OAAO;AACrB,YAAO,MAAM,qCAAqC,OAAO,KAAK,CAAC;AAC/D,YAAO,QAAQ,QAAQ,GAAG;;AAI9B,WAAO;;GAET,IAAI,QAAQ,MAAM,OAAO,UAAU;AACjC,WAAO,QAAQ,IAAI,QAAQ,MAAM,OAAO,SAAS;;GAEpD,CAAC;AAGJ,SAAO,cAAc;;CAGvB,qBAAqB,QAAgC;AACnD,OAAK,sBAAsB;;CAG7B,MAAM,SACJ,UACA,KACiB;AACjB,MAAI,CAAC,KAAK,oBACR,OAAM,YAAY,qBAAqB;EAEzC,MAAM,SAAS,KAAK,oBAAoB,IAAI;AAE5C,MAAI,CAAC,OACH,OAAM,YAAY,cAAc;EAGlC,MAAM,EAAE,IAAI,qBAAqB;EACjC,MAAM,YAAY,YAAY;AAE9B,SAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,UAAU,iBAAiB;AAC/B,qBAAiB,OAAO,UAAU;AAClC,2BAAO,IAAI,MAAM,sBAAsB,WAAW,CAAC;MAClD,IAAM;AAET,oBAAiB,IAAI,WAAW;IAAE;IAAS;IAAQ;IAAS,CAAC;AAE7D,MAAG,KACD,KAAK,UAAU;IACb,MAAM;IACN;IACA,MAAM;IACP,CAAC,CACH;IACD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","names":[],"sources":["../../../src/plugin/interceptors/cache.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"cache.js","names":[],"sources":["../../../src/plugin/interceptors/cache.ts"],"sourcesContent":["import type { CacheConfig } from \"shared\";\nimport type { CacheManager } from \"../../cache\";\nimport type { ExecutionInterceptor, InterceptorContext } from \"./types\";\n\n// interceptor to handle caching logic\nexport class CacheInterceptor implements ExecutionInterceptor {\n constructor(\n private cacheManager: CacheManager,\n private config: CacheConfig,\n ) {}\n\n async intercept<T>(\n fn: () => Promise<T>,\n context: InterceptorContext,\n ): Promise<T> {\n // if cache disabled, ignore\n if (!this.config.enabled || !this.config.cacheKey?.length) {\n return fn();\n }\n\n return this.cacheManager.getOrExecute(\n this.config.cacheKey,\n fn,\n context.userKey,\n { ttl: this.config.ttl },\n );\n }\n}\n"],"mappings":";AAKA,IAAa,mBAAb,MAA8D;CAC5D,YACE,AAAQ,cACR,AAAQ,QACR;EAFQ;EACA;;CAGV,MAAM,UACJ,IACA,SACY;AAEZ,MAAI,CAAC,KAAK,OAAO,WAAW,CAAC,KAAK,OAAO,UAAU,OACjD,QAAO,IAAI;AAGb,SAAO,KAAK,aAAa,aACvB,KAAK,OAAO,UACZ,IACA,QAAQ,SACR,EAAE,KAAK,KAAK,OAAO,KAAK,CACzB"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createLogger } from "../../logging/logger.js";
|
|
2
|
+
|
|
1
3
|
//#region src/plugin/interceptors/retry.ts
|
|
4
|
+
const logger = createLogger("interceptors:retry");
|
|
2
5
|
var RetryInterceptor = class {
|
|
3
6
|
constructor(config) {
|
|
4
7
|
this.attempts = config.attempts ?? 3;
|
|
@@ -8,10 +11,15 @@ var RetryInterceptor = class {
|
|
|
8
11
|
async intercept(fn, context) {
|
|
9
12
|
let lastError;
|
|
10
13
|
for (let attempt = 1; attempt <= this.attempts; attempt++) try {
|
|
11
|
-
|
|
14
|
+
const result = await fn();
|
|
15
|
+
if (attempt > 1) logger.event()?.setExecution({ retry_attempts: attempt - 1 });
|
|
16
|
+
return result;
|
|
12
17
|
} catch (error) {
|
|
13
18
|
lastError = error;
|
|
14
|
-
if (attempt === this.attempts)
|
|
19
|
+
if (attempt === this.attempts) {
|
|
20
|
+
logger.event()?.setExecution({ retry_attempts: attempt - 1 });
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
15
23
|
if (context.signal?.aborted) throw error;
|
|
16
24
|
const delay = this.calculateDelay(attempt);
|
|
17
25
|
await this.sleep(delay);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.js","names":[],"sources":["../../../src/plugin/interceptors/retry.ts"],"sourcesContent":["import type { RetryConfig } from \"shared\";\nimport type {
|
|
1
|
+
{"version":3,"file":"retry.js","names":[],"sources":["../../../src/plugin/interceptors/retry.ts"],"sourcesContent":["import type { RetryConfig } from \"shared\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { ExecutionInterceptor, InterceptorContext } from \"./types\";\n\nconst logger = createLogger(\"interceptors:retry\");\n\n// interceptor to handle retry logic\nexport class RetryInterceptor implements ExecutionInterceptor {\n private attempts: number;\n private initialDelay: number;\n private maxDelay: number;\n\n constructor(config: RetryConfig) {\n this.attempts = config.attempts ?? 3;\n this.initialDelay = config.initialDelay ?? 1000;\n this.maxDelay = config.maxDelay ?? 30000;\n }\n\n async intercept<T>(\n fn: () => Promise<T>,\n context: InterceptorContext,\n ): Promise<T> {\n let lastError: Error | unknown;\n\n for (let attempt = 1; attempt <= this.attempts; attempt++) {\n try {\n const result = await fn();\n\n if (attempt > 1) {\n logger.event()?.setExecution({\n retry_attempts: attempt - 1,\n });\n }\n\n return result;\n } catch (error) {\n lastError = error;\n\n // last attempt, rethrow the error\n if (attempt === this.attempts) {\n logger.event()?.setExecution({\n retry_attempts: attempt - 1,\n });\n throw error;\n }\n\n // don't retry if was already aborted\n if (context.signal?.aborted) {\n throw error;\n }\n\n const delay = this.calculateDelay(attempt);\n await this.sleep(delay);\n }\n }\n\n // type guard\n throw lastError;\n }\n\n private calculateDelay(attempt: number): number {\n // exponential backoff\n const delay = this.initialDelay * 2 ** (attempt - 1);\n\n // max delay cap\n return Math.min(delay, this.maxDelay);\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":";;;AAIA,MAAM,SAAS,aAAa,qBAAqB;AAGjD,IAAa,mBAAb,MAA8D;CAK5D,YAAY,QAAqB;AAC/B,OAAK,WAAW,OAAO,YAAY;AACnC,OAAK,eAAe,OAAO,gBAAgB;AAC3C,OAAK,WAAW,OAAO,YAAY;;CAGrC,MAAM,UACJ,IACA,SACY;EACZ,IAAI;AAEJ,OAAK,IAAI,UAAU,GAAG,WAAW,KAAK,UAAU,UAC9C,KAAI;GACF,MAAM,SAAS,MAAM,IAAI;AAEzB,OAAI,UAAU,EACZ,QAAO,OAAO,EAAE,aAAa,EAC3B,gBAAgB,UAAU,GAC3B,CAAC;AAGJ,UAAO;WACA,OAAO;AACd,eAAY;AAGZ,OAAI,YAAY,KAAK,UAAU;AAC7B,WAAO,OAAO,EAAE,aAAa,EAC3B,gBAAgB,UAAU,GAC3B,CAAC;AACF,UAAM;;AAIR,OAAI,QAAQ,QAAQ,QAClB,OAAM;GAGR,MAAM,QAAQ,KAAK,eAAe,QAAQ;AAC1C,SAAM,KAAK,MAAM,MAAM;;AAK3B,QAAM;;CAGR,AAAQ,eAAe,SAAyB;EAE9C,MAAM,QAAQ,KAAK,eAAe,MAAM,UAAU;AAGlD,SAAO,KAAK,IAAI,OAAO,KAAK,SAAS;;CAGvC,AAAQ,MAAM,IAA2B;AACvC,SAAO,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC"}
|
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
import { SpanStatusCode } from "../../telemetry/index.js";
|
|
2
2
|
|
|
3
3
|
//#region src/plugin/interceptors/telemetry.ts
|
|
4
|
-
/**
|
|
5
|
-
* Interceptor to automatically instrument plugin executions with telemetry spans.
|
|
6
|
-
* Wraps the execution in a span and handles success/error status.
|
|
7
|
-
*/
|
|
8
4
|
var TelemetryInterceptor = class {
|
|
9
5
|
constructor(telemetry, config) {
|
|
10
6
|
this.telemetry = telemetry;
|
|
11
7
|
this.config = config;
|
|
12
8
|
}
|
|
13
|
-
async intercept(fn,
|
|
9
|
+
async intercept(fn, context) {
|
|
14
10
|
const spanName = this.config?.spanName || "plugin.execute";
|
|
11
|
+
if (context.signal?.aborted) throw new Error("Operation aborted before execution");
|
|
15
12
|
return this.telemetry.startActiveSpan(spanName, { attributes: this.config?.attributes }, async (span) => {
|
|
13
|
+
let abortHandler;
|
|
14
|
+
let isAborted = false;
|
|
15
|
+
if (context.signal) {
|
|
16
|
+
abortHandler = () => {
|
|
17
|
+
if (!span.isRecording()) return;
|
|
18
|
+
isAborted = true;
|
|
19
|
+
span.setAttribute("cancelled", true);
|
|
20
|
+
span.setStatus({
|
|
21
|
+
code: SpanStatusCode.ERROR,
|
|
22
|
+
message: "Operation cancelled by client"
|
|
23
|
+
});
|
|
24
|
+
span.end();
|
|
25
|
+
};
|
|
26
|
+
context.signal.addEventListener("abort", abortHandler, { once: true });
|
|
27
|
+
}
|
|
16
28
|
try {
|
|
17
29
|
const result = await fn();
|
|
18
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
30
|
+
if (!isAborted) span.setStatus({ code: SpanStatusCode.OK });
|
|
19
31
|
return result;
|
|
20
32
|
} catch (error) {
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
if (!isAborted) {
|
|
34
|
+
span.recordException(error);
|
|
35
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
36
|
+
}
|
|
23
37
|
throw error;
|
|
24
38
|
} finally {
|
|
25
|
-
|
|
39
|
+
if (abortHandler && context.signal) context.signal.removeEventListener("abort", abortHandler);
|
|
40
|
+
if (!isAborted) span.end();
|
|
26
41
|
}
|
|
27
42
|
});
|
|
28
43
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry.js","names":[],"sources":["../../../src/plugin/interceptors/telemetry.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"telemetry.js","names":[],"sources":["../../../src/plugin/interceptors/telemetry.ts"],"sourcesContent":["import type { TelemetryConfig } from \"shared\";\nimport type { ITelemetry, Span } from \"../../telemetry\";\nimport { SpanStatusCode } from \"../../telemetry\";\nimport type { ExecutionInterceptor, InterceptorContext } from \"./types\";\n\nexport class TelemetryInterceptor implements ExecutionInterceptor {\n constructor(\n private telemetry: ITelemetry,\n private config?: TelemetryConfig,\n ) {}\n\n async intercept<T>(\n fn: () => Promise<T>,\n context: InterceptorContext,\n ): Promise<T> {\n const spanName = this.config?.spanName || \"plugin.execute\";\n\n // abort operation if signal is aborted\n if (context.signal?.aborted) {\n throw new Error(\"Operation aborted before execution\");\n }\n\n return this.telemetry.startActiveSpan(\n spanName,\n { attributes: this.config?.attributes },\n async (span: Span) => {\n let abortHandler: (() => void) | undefined;\n let isAborted = false;\n\n if (context.signal) {\n abortHandler = () => {\n // abort span if not recording\n if (!span.isRecording()) return;\n isAborted = true;\n span.setAttribute(\"cancelled\", true);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: \"Operation cancelled by client\",\n });\n span.end();\n };\n context.signal.addEventListener(\"abort\", abortHandler, {\n once: true,\n });\n }\n\n try {\n const result = await fn();\n if (!isAborted) {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n return result;\n } catch (error) {\n if (!isAborted) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n throw error;\n } finally {\n if (abortHandler && context.signal) {\n context.signal.removeEventListener(\"abort\", abortHandler);\n }\n if (!isAborted) {\n span.end();\n }\n }\n },\n );\n }\n}\n"],"mappings":";;;AAKA,IAAa,uBAAb,MAAkE;CAChE,YACE,AAAQ,WACR,AAAQ,QACR;EAFQ;EACA;;CAGV,MAAM,UACJ,IACA,SACY;EACZ,MAAM,WAAW,KAAK,QAAQ,YAAY;AAG1C,MAAI,QAAQ,QAAQ,QAClB,OAAM,IAAI,MAAM,qCAAqC;AAGvD,SAAO,KAAK,UAAU,gBACpB,UACA,EAAE,YAAY,KAAK,QAAQ,YAAY,EACvC,OAAO,SAAe;GACpB,IAAI;GACJ,IAAI,YAAY;AAEhB,OAAI,QAAQ,QAAQ;AAClB,yBAAqB;AAEnB,SAAI,CAAC,KAAK,aAAa,CAAE;AACzB,iBAAY;AACZ,UAAK,aAAa,aAAa,KAAK;AACpC,UAAK,UAAU;MACb,MAAM,eAAe;MACrB,SAAS;MACV,CAAC;AACF,UAAK,KAAK;;AAEZ,YAAQ,OAAO,iBAAiB,SAAS,cAAc,EACrD,MAAM,MACP,CAAC;;AAGJ,OAAI;IACF,MAAM,SAAS,MAAM,IAAI;AACzB,QAAI,CAAC,UACH,MAAK,UAAU,EAAE,MAAM,eAAe,IAAI,CAAC;AAE7C,WAAO;YACA,OAAO;AACd,QAAI,CAAC,WAAW;AACd,UAAK,gBAAgB,MAAe;AACpC,UAAK,UAAU,EAAE,MAAM,eAAe,OAAO,CAAC;;AAEhD,UAAM;aACE;AACR,QAAI,gBAAgB,QAAQ,OAC1B,SAAQ,OAAO,oBAAoB,SAAS,aAAa;AAE3D,QAAI,CAAC,UACH,MAAK,KAAK;;IAIjB"}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { createLogger } from "../../logging/logger.js";
|
|
2
|
+
|
|
1
3
|
//#region src/plugin/interceptors/timeout.ts
|
|
4
|
+
const logger = createLogger("interceptors:timeout");
|
|
2
5
|
var TimeoutInterceptor = class {
|
|
3
6
|
constructor(timeoutMs) {
|
|
4
7
|
this.timeoutMs = timeoutMs;
|
|
5
8
|
}
|
|
6
9
|
async intercept(fn, context) {
|
|
10
|
+
logger.event()?.setExecution({ timeout_ms: this.timeoutMs });
|
|
7
11
|
const timeoutController = new AbortController();
|
|
8
12
|
const timeoutId = setTimeout(() => {
|
|
9
13
|
timeoutController.abort(/* @__PURE__ */ new Error(`Operation timed out after ${this.timeoutMs} ms`));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeout.js","names":[],"sources":["../../../src/plugin/interceptors/timeout.ts"],"sourcesContent":["import type {
|
|
1
|
+
{"version":3,"file":"timeout.js","names":[],"sources":["../../../src/plugin/interceptors/timeout.ts"],"sourcesContent":["import { createLogger } from \"../../logging/logger\";\nimport type { ExecutionInterceptor, InterceptorContext } from \"./types\";\n\nconst logger = createLogger(\"interceptors:timeout\");\n\n// interceptor to handle timeout logic\nexport class TimeoutInterceptor implements ExecutionInterceptor {\n constructor(private timeoutMs: number) {}\n\n async intercept<T>(\n fn: () => Promise<T>,\n context: InterceptorContext,\n ): Promise<T> {\n logger.event()?.setExecution({\n timeout_ms: this.timeoutMs,\n });\n\n // create timeout signal\n const timeoutController = new AbortController();\n const timeoutId = setTimeout(() => {\n timeoutController.abort(\n new Error(`Operation timed out after ${this.timeoutMs} ms`),\n );\n }, this.timeoutMs);\n\n try {\n // combine user signal (if exists) with timeout signal\n const combinedSignal = context.signal\n ? this._combineSignals([context.signal, timeoutController.signal])\n : timeoutController.signal;\n\n // execute function with combined signal\n context.signal = combinedSignal;\n return await fn();\n } finally {\n // cleanup timeout\n clearTimeout(timeoutId);\n }\n }\n\n private _combineSignals(signals: AbortSignal[]): AbortSignal {\n const controller = new AbortController();\n for (const signal of signals) {\n if (signal.aborted) {\n controller.abort(signal.reason);\n break;\n }\n signal.addEventListener(\n \"abort\",\n () => {\n controller.abort(signal.reason);\n },\n { once: true },\n );\n }\n return controller.signal;\n }\n}\n"],"mappings":";;;AAGA,MAAM,SAAS,aAAa,uBAAuB;AAGnD,IAAa,qBAAb,MAAgE;CAC9D,YAAY,AAAQ,WAAmB;EAAnB;;CAEpB,MAAM,UACJ,IACA,SACY;AACZ,SAAO,OAAO,EAAE,aAAa,EAC3B,YAAY,KAAK,WAClB,CAAC;EAGF,MAAM,oBAAoB,IAAI,iBAAiB;EAC/C,MAAM,YAAY,iBAAiB;AACjC,qBAAkB,sBAChB,IAAI,MAAM,6BAA6B,KAAK,UAAU,KAAK,CAC5D;KACA,KAAK,UAAU;AAElB,MAAI;AAOF,WAAQ,SALe,QAAQ,SAC3B,KAAK,gBAAgB,CAAC,QAAQ,QAAQ,kBAAkB,OAAO,CAAC,GAChE,kBAAkB;AAItB,UAAO,MAAM,IAAI;YACT;AAER,gBAAa,UAAU;;;CAI3B,AAAQ,gBAAgB,SAAqC;EAC3D,MAAM,aAAa,IAAI,iBAAiB;AACxC,OAAK,MAAM,UAAU,SAAS;AAC5B,OAAI,OAAO,SAAS;AAClB,eAAW,MAAM,OAAO,OAAO;AAC/B;;AAEF,UAAO,iBACL,eACM;AACJ,eAAW,MAAM,OAAO,OAAO;MAEjC,EAAE,MAAM,MAAM,CACf;;AAEH,SAAO,WAAW"}
|
package/dist/plugin/plugin.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BasePlugin, BasePluginConfig, IAppResponse, PluginEndpointMap, PluginPhase, RouteConfig } from "../shared/src/plugin.js";
|
|
2
2
|
import { PluginExecutionSettings, StreamExecuteHandler, StreamExecutionSettings } from "../shared/src/execute.js";
|
|
3
|
-
import { ITelemetry } from "../telemetry/types.js";
|
|
4
3
|
import { AppManager } from "../app/index.js";
|
|
5
4
|
import { CacheManager } from "../cache/index.js";
|
|
6
5
|
import { StreamManager } from "../stream/stream-manager.js";
|
|
6
|
+
import { ITelemetry } from "../telemetry/types.js";
|
|
7
7
|
import { DevFileReader } from "./dev-reader.js";
|
|
8
8
|
import express from "express";
|
|
9
9
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","names":[],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;uBA6DsB,uBACJ,mBAAmB,6BACxB;oBAgBmB;;EAlBV,UAAM,KAAA,EAKT,YALS;EAAA,UAAA,GAAA,EAMX,UANW;YACV,aAAA,EAMS,aANT;YAAmB,aAAA,EAOV,aAPU;YAiBL,SAAA,EATT,UASS;qBAbb,OAAA,EAAA,MAAA,EAAA;;UAEQ,mBAAA;SACA,KAAA,EAOX,WAPW;MACJ,EAAA,MAAA;aAMP,CAAA,MAAA,EAGgB,OAHhB;aAGgB,CAAA,CAAA,EAAA,IAAA;cAeN,CAAA,CAAA,EAAR,OAAA,CAAQ,MAAA,CAAA,EAAA,IAAA;OAIb,CAAA,CAAA,EAAA,OAAA,CAAA,IAAA,CAAA;cAEK,CAAA,CAAA,EAAA,iBAAA;uBAqCI,CAAA,CAAA,EAAA,IAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAR,OAAA,CAAQ;;;;;;;kCA6Db,kBACD,qBAAqB,aAChB,4CACO;qCA2DF,gBAAgB,QAAQ,aAC7B,4CAER,QAAQ;;sCA0BD,OAAA,CAAQ,gBACR"}
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { normalizeTelemetryOptions } from "../telemetry/config.js";
|
|
2
|
+
import { createLogger } from "../logging/logger.js";
|
|
2
3
|
import { TelemetryManager } from "../telemetry/telemetry-manager.js";
|
|
3
4
|
import "../telemetry/index.js";
|
|
5
|
+
import { AuthenticationError } from "../errors/authentication.js";
|
|
6
|
+
import { init_errors } from "../errors/index.js";
|
|
4
7
|
import { validateEnv } from "../utils/env-validator.js";
|
|
5
8
|
import { deepMerge } from "../utils/merge.js";
|
|
6
9
|
import { ServiceContext } from "../context/service-context.js";
|
|
7
10
|
import { getCurrentUserId, runInUserContext } from "../context/execution-context.js";
|
|
8
11
|
import { init_context } from "../context/index.js";
|
|
9
|
-
import { CacheManager } from "../cache/index.js";
|
|
10
12
|
import { AppManager } from "../app/index.js";
|
|
13
|
+
import { CacheManager } from "../cache/index.js";
|
|
11
14
|
import { StreamManager } from "../stream/stream-manager.js";
|
|
12
15
|
import "../stream/index.js";
|
|
13
16
|
import { DevFileReader } from "./dev-reader.js";
|
|
@@ -18,6 +21,8 @@ import { TimeoutInterceptor } from "./interceptors/timeout.js";
|
|
|
18
21
|
|
|
19
22
|
//#region src/plugin/plugin.ts
|
|
20
23
|
init_context();
|
|
24
|
+
init_errors();
|
|
25
|
+
const logger = createLogger("plugin");
|
|
21
26
|
/**
|
|
22
27
|
* Methods that should not be proxied by asUser().
|
|
23
28
|
* These are lifecycle/internal methods that don't make sense
|
|
@@ -94,11 +99,11 @@ var Plugin = class {
|
|
|
94
99
|
const userId = req.headers["x-forwarded-user"];
|
|
95
100
|
const isDev = process.env.NODE_ENV === "development";
|
|
96
101
|
if (!token && isDev) {
|
|
97
|
-
|
|
102
|
+
logger.warn("asUser() called without user token in development mode. Using service principal.");
|
|
98
103
|
return this;
|
|
99
104
|
}
|
|
100
|
-
if (!token) throw
|
|
101
|
-
if (!userId && !isDev) throw
|
|
105
|
+
if (!token) throw AuthenticationError.missingToken("user token");
|
|
106
|
+
if (!userId && !isDev) throw AuthenticationError.missingUserId();
|
|
102
107
|
const effectiveUserId = userId || "dev-user";
|
|
103
108
|
const userContext = ServiceContext.createUserContext(token, effectiveUserId);
|
|
104
109
|
return this.createUserContextProxy(userContext);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":[],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport {\n ServiceContext,\n getCurrentUserId,\n runInUserContext,\n type UserContext,\n} from \"../context\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge, validateEnv } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n InterceptorContext,\n ExecutionInterceptor,\n} from \"./interceptors/types\";\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"validateEnv\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"abortActiveOperations\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry: ITelemetry;\n protected abstract envVars: string[];\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n static phase: PluginPhase = \"normal\";\n name: string;\n\n constructor(protected config: TConfig) {\n this.name = config.name ?? \"plugin\";\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.streamManager = new StreamManager();\n this.cache = CacheManager.getInstanceSync();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n\n this.isReady = true;\n }\n\n validateEnv() {\n validateEnv(this.envVars);\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Execute operations using the user's identity from the request.\n *\n * Returns a scoped instance of this plugin where all method calls\n * will execute with the user's Databricks credentials instead of\n * the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A scoped plugin instance that executes as the user\n * @throws Error if user token is not available in request headers\n *\n * @example\n * ```typescript\n * // In route handler - execute query as the requesting user\n * router.post('/users/me/query/:key', async (req, res) => {\n * const result = await this.asUser(req).query(req.params.key)\n * res.json(result)\n * })\n *\n * // Mixed execution in same handler\n * router.post('/dashboard', async (req, res) => {\n * const [systemData, userData] = await Promise.all([\n * this.getSystemStats(), // Service principal\n * this.asUser(req).getUserPreferences(), // User context\n * ])\n * res.json({ systemData, userData })\n * })\n * ```\n */\n asUser(req: express.Request): this {\n const token = req.headers[\"x-forwarded-access-token\"] as string;\n const userId = req.headers[\"x-forwarded-user\"] as string;\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, fall back to service principal\n // since there's no user token available\n if (!token && isDev) {\n console.warn(\n \"[AppKit] asUser() called without user token in development mode. \" +\n \"Using service principal.\",\n );\n\n return this;\n }\n\n if (!token) {\n throw new Error(\n \"User token not available in request headers. \" +\n \"Ensure the request has the x-forwarded-access-token header.\",\n );\n }\n\n if (!userId && !isDev) {\n throw new Error(\n \"User ID not available in request headers. \" +\n \"Ensure the request has the x-forwarded-user header.\",\n );\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n );\n\n // Return a proxy that wraps method calls in user context\n return this.createUserContextProxy(userContext);\n }\n\n /**\n * Creates a proxy that wraps method calls in a user context.\n * This allows all plugin methods to automatically use the user's\n * Databricks credentials.\n */\n private createUserContextProxy(userContext: UserContext): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") {\n return value;\n }\n\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop)) {\n return value;\n }\n\n return (...args: unknown[]) => {\n return runInUserContext(userContext, () => value.apply(target, args));\n };\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // Get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors\n const result = await self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client\n await this.streamManager.stream(res, asyncWrapperFn, streamConfig);\n }\n\n // single sync execution with interceptors\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<T | undefined> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // Get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n return await this._executeWithInterceptors(fn, interceptors, context);\n } catch (_error) {\n // production-safe, don't crash sdk\n return undefined;\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n this.registerEndpoint(name, `/api/${this.name}${path}`);\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n // Only add telemetry interceptor if traces are enabled\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":[],"sources":["../../src/plugin/plugin.ts"],"sourcesContent":["import type express from \"express\";\nimport type {\n BasePlugin,\n BasePluginConfig,\n IAppResponse,\n PluginEndpointMap,\n PluginExecuteConfig,\n PluginExecutionSettings,\n PluginPhase,\n RouteConfig,\n StreamExecuteHandler,\n StreamExecutionSettings,\n} from \"shared\";\nimport { AppManager } from \"../app\";\nimport { CacheManager } from \"../cache\";\nimport {\n getCurrentUserId,\n runInUserContext,\n ServiceContext,\n type UserContext,\n} from \"../context\";\nimport { AuthenticationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport { StreamManager } from \"../stream\";\nimport {\n type ITelemetry,\n normalizeTelemetryOptions,\n TelemetryManager,\n} from \"../telemetry\";\nimport { deepMerge, validateEnv } from \"../utils\";\nimport { DevFileReader } from \"./dev-reader\";\nimport { CacheInterceptor } from \"./interceptors/cache\";\nimport { RetryInterceptor } from \"./interceptors/retry\";\nimport { TelemetryInterceptor } from \"./interceptors/telemetry\";\nimport { TimeoutInterceptor } from \"./interceptors/timeout\";\nimport type {\n ExecutionInterceptor,\n InterceptorContext,\n} from \"./interceptors/types\";\n\nconst logger = createLogger(\"plugin\");\n\n/**\n * Methods that should not be proxied by asUser().\n * These are lifecycle/internal methods that don't make sense\n * to execute in a user context.\n */\nconst EXCLUDED_FROM_PROXY = new Set([\n // Lifecycle methods\n \"setup\",\n \"shutdown\",\n \"validateEnv\",\n \"injectRoutes\",\n \"getEndpoints\",\n \"abortActiveOperations\",\n // asUser itself - prevent chaining like .asUser().asUser()\n \"asUser\",\n // Internal methods\n \"constructor\",\n]);\n\nexport abstract class Plugin<\n TConfig extends BasePluginConfig = BasePluginConfig,\n> implements BasePlugin\n{\n protected isReady = false;\n protected cache: CacheManager;\n protected app: AppManager;\n protected devFileReader: DevFileReader;\n protected streamManager: StreamManager;\n protected telemetry: ITelemetry;\n protected abstract envVars: string[];\n\n /** Registered endpoints for this plugin */\n private registeredEndpoints: PluginEndpointMap = {};\n\n static phase: PluginPhase = \"normal\";\n name: string;\n\n constructor(protected config: TConfig) {\n this.name = config.name ?? \"plugin\";\n this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);\n this.streamManager = new StreamManager();\n this.cache = CacheManager.getInstanceSync();\n this.app = new AppManager();\n this.devFileReader = DevFileReader.getInstance();\n\n this.isReady = true;\n }\n\n validateEnv() {\n validateEnv(this.envVars);\n }\n\n injectRoutes(_: express.Router) {\n return;\n }\n\n async setup() {}\n\n getEndpoints(): PluginEndpointMap {\n return this.registeredEndpoints;\n }\n\n abortActiveOperations(): void {\n this.streamManager.abortAll();\n }\n\n /**\n * Execute operations using the user's identity from the request.\n *\n * Returns a scoped instance of this plugin where all method calls\n * will execute with the user's Databricks credentials instead of\n * the service principal.\n *\n * @param req - The Express request containing the user token in headers\n * @returns A scoped plugin instance that executes as the user\n * @throws Error if user token is not available in request headers\n *\n * @example\n * ```typescript\n * // In route handler - execute query as the requesting user\n * router.post('/users/me/query/:key', async (req, res) => {\n * const result = await this.asUser(req).query(req.params.key)\n * res.json(result)\n * })\n *\n * // Mixed execution in same handler\n * router.post('/dashboard', async (req, res) => {\n * const [systemData, userData] = await Promise.all([\n * this.getSystemStats(), // Service principal\n * this.asUser(req).getUserPreferences(), // User context\n * ])\n * res.json({ systemData, userData })\n * })\n * ```\n */\n asUser(req: express.Request): this {\n const token = req.headers[\"x-forwarded-access-token\"] as string;\n const userId = req.headers[\"x-forwarded-user\"] as string;\n const isDev = process.env.NODE_ENV === \"development\";\n\n // In local development, fall back to service principal\n // since there's no user token available\n if (!token && isDev) {\n logger.warn(\n \"asUser() called without user token in development mode. Using service principal.\",\n );\n\n return this;\n }\n\n if (!token) {\n throw AuthenticationError.missingToken(\"user token\");\n }\n\n if (!userId && !isDev) {\n throw AuthenticationError.missingUserId();\n }\n\n const effectiveUserId = userId || \"dev-user\";\n\n const userContext = ServiceContext.createUserContext(\n token,\n effectiveUserId,\n );\n\n // Return a proxy that wraps method calls in user context\n return this.createUserContextProxy(userContext);\n }\n\n /**\n * Creates a proxy that wraps method calls in a user context.\n * This allows all plugin methods to automatically use the user's\n * Databricks credentials.\n */\n private createUserContextProxy(userContext: UserContext): this {\n return new Proxy(this, {\n get: (target, prop, receiver) => {\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value !== \"function\") {\n return value;\n }\n\n if (typeof prop === \"string\" && EXCLUDED_FROM_PROXY.has(prop)) {\n return value;\n }\n\n return (...args: unknown[]) => {\n return runInUserContext(userContext, () => value.apply(target, args));\n };\n },\n }) as this;\n }\n\n // streaming execution with interceptors\n protected async executeStream<T>(\n res: IAppResponse,\n fn: StreamExecuteHandler<T>,\n options: StreamExecutionSettings,\n userKey?: string,\n ) {\n // destructure options\n const {\n stream: streamConfig,\n default: defaultConfig,\n user: userConfig,\n } = options;\n\n // build execution options\n const executeConfig = this._buildExecutionConfig({\n default: defaultConfig,\n user: userConfig,\n });\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const self = this;\n\n // wrapper function to ensure it returns a generator\n const asyncWrapperFn = async function* (streamSignal?: AbortSignal) {\n // build execution context\n const context: InterceptorContext = {\n signal: streamSignal,\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n // build interceptors\n const interceptors = self._buildInterceptors(executeConfig);\n\n // wrap the function to ensure it returns a promise\n const wrappedFn = async () => {\n const result = await fn(context.signal);\n return result;\n };\n\n // execute the function with interceptors\n const result = await self._executeWithInterceptors(\n wrappedFn as (signal?: AbortSignal) => Promise<T>,\n interceptors,\n context,\n );\n\n // check if result is a generator\n if (self._checkIfGenerator(result)) {\n yield* result;\n } else {\n yield result;\n }\n };\n\n // stream the result to the client\n await this.streamManager.stream(res, asyncWrapperFn, streamConfig);\n }\n\n // single sync execution with interceptors\n protected async execute<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n options: PluginExecutionSettings,\n userKey?: string,\n ): Promise<T | undefined> {\n const executeConfig = this._buildExecutionConfig(options);\n\n const interceptors = this._buildInterceptors(executeConfig);\n\n // get user key from context if not provided\n const effectiveUserKey = userKey ?? getCurrentUserId();\n\n const context: InterceptorContext = {\n metadata: new Map(),\n userKey: effectiveUserKey,\n };\n\n try {\n return await this._executeWithInterceptors(fn, interceptors, context);\n } catch (_error) {\n // production-safe, don't crash sdk\n return undefined;\n }\n }\n\n protected registerEndpoint(name: string, path: string): void {\n this.registeredEndpoints[name] = path;\n }\n\n protected route<_TResponse>(\n router: express.Router,\n config: RouteConfig,\n ): void {\n const { name, method, path, handler } = config;\n\n router[method](path, handler);\n\n this.registerEndpoint(name, `/api/${this.name}${path}`);\n }\n\n // build execution options by merging defaults, plugin config, and user overrides\n private _buildExecutionConfig(\n options: PluginExecutionSettings,\n ): PluginExecuteConfig {\n const { default: methodDefaults, user: userOverride } = options;\n\n // Merge: method defaults <- plugin config <- user override (highest priority)\n return deepMerge(\n deepMerge(methodDefaults, this.config),\n userOverride ?? {},\n ) as PluginExecuteConfig;\n }\n\n // build interceptors based on execute options\n private _buildInterceptors(\n options: PluginExecuteConfig,\n ): ExecutionInterceptor[] {\n const interceptors: ExecutionInterceptor[] = [];\n\n // order matters: telemetry → timeout → retry → cache (innermost to outermost)\n\n const telemetryConfig = normalizeTelemetryOptions(this.config.telemetry);\n if (\n telemetryConfig.traces &&\n (options.telemetryInterceptor?.enabled ?? true)\n ) {\n interceptors.push(\n new TelemetryInterceptor(this.telemetry, options.telemetryInterceptor),\n );\n }\n\n if (options.timeout && options.timeout > 0) {\n interceptors.push(new TimeoutInterceptor(options.timeout));\n }\n\n if (\n options.retry?.enabled &&\n options.retry.attempts &&\n options.retry.attempts > 1\n ) {\n interceptors.push(new RetryInterceptor(options.retry));\n }\n\n if (options.cache?.enabled && options.cache.cacheKey?.length) {\n interceptors.push(new CacheInterceptor(this.cache, options.cache));\n }\n\n return interceptors;\n }\n\n // execute method wrapped with interceptors\n private async _executeWithInterceptors<T>(\n fn: (signal?: AbortSignal) => Promise<T>,\n interceptors: ExecutionInterceptor[],\n context: InterceptorContext,\n ): Promise<T> {\n // no interceptors, execute directly\n if (interceptors.length === 0) {\n return fn(context.signal);\n }\n // build nested execution chain from interceptors\n let wrappedFn = () => fn(context.signal);\n\n // wrap each interceptor around the previous function\n for (const interceptor of interceptors) {\n const previousFn = wrappedFn;\n wrappedFn = () => interceptor.intercept(previousFn, context);\n }\n\n return wrappedFn();\n }\n\n private _checkIfGenerator(\n result: any,\n ): result is AsyncGenerator<any, void, unknown> {\n return (\n result && typeof result === \"object\" && Symbol.asyncIterator in result\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/server/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AAkCA;;;;;;;;;;;;AAAwC,cAA3B,YAAA,SAAqB,MAAA,CAAM;EA0S3B,OAAA,cAGZ,EAAA;IAAA,SAAA,EAAA,OAAA;IAHkB,IAAA,EAAA,MAAA;IAAA,IAAA,EAAA,MAAA;;EAAA,IAAA,EAAA,QAAA;;;;;;oBA7RS;;gBAEZ;sBAEM;;WAaT;;;;;;;;;gBAAA;;;;;;;;;;;;WA0BI,QAAQ,OAAA,CAAQ;;;;;;;;;eA8ClB;;;;;;;;mBAmBI,OAAA,CAAQ;;;;;;;;;;;;;;;;;;;;;;cAiLd,QAAM,gBAAA,cAAA"}
|