@intelligems/sst 2.49.6-ig.1 → 2.49.6-ig.10
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/cli/commands/dev.js +69 -3
- package/cli/sst.js +0 -1
- package/iot.d.ts +31 -0
- package/iot.js +169 -8
- package/package.json +2 -2
- package/runtime/debug-bridge-logging.d.ts +24 -0
- package/runtime/debug-bridge-logging.js +98 -0
- package/runtime/debug-file-logger.d.ts +80 -0
- package/runtime/debug-file-logger.js +148 -0
- package/runtime/event-trace-logging.d.ts +27 -0
- package/runtime/event-trace-logging.js +69 -0
- package/runtime/handlers/node.js +62 -21
- package/runtime/handlers.d.ts +6 -2
- package/runtime/handlers.js +130 -59
- package/runtime/iot.js +31 -4
- package/runtime/mono-build-config.d.ts +58 -0
- package/runtime/mono-build-config.js +104 -0
- package/runtime/request-utils.d.ts +21 -0
- package/runtime/request-utils.js +102 -0
- package/runtime/runtime.d.ts +1 -0
- package/runtime/server.d.ts +4 -1
- package/runtime/server.js +122 -29
- package/runtime/worker-pool-logging.d.ts +29 -0
- package/runtime/worker-pool-logging.js +185 -0
- package/runtime/workers.d.ts +39 -7
- package/runtime/workers.js +831 -38
- package/support/bridge/live-lambda.mjs +38 -38
- package/support/nodejs-runtime/index.mjs +44 -11
- package/support/python-runtime/runtime.py +116 -45
- package/README.md +0 -43
- package/package.json.bak +0 -156
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// Active loggers registry (for cleanup)
|
|
4
|
+
const activeLoggers = new Map();
|
|
5
|
+
// Cleanup on process exit
|
|
6
|
+
process.on("exit", () => {
|
|
7
|
+
for (const stream of activeLoggers.values()) {
|
|
8
|
+
try {
|
|
9
|
+
stream.end();
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Create a debug file logger with consistent formatting
|
|
16
|
+
*
|
|
17
|
+
* Features:
|
|
18
|
+
* - File-based logging to configurable path
|
|
19
|
+
* - Optional enable/disable via environment variable
|
|
20
|
+
* - Session start/end markers
|
|
21
|
+
* - Consistent timestamp formatting (HH:MM:SS.mmm)
|
|
22
|
+
* - Automatic directory creation
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const logger = createDebugFileLogger({
|
|
27
|
+
* filePath: ".sst/debug-workers.log",
|
|
28
|
+
* envVar: "SST_DEBUG_WORKERS",
|
|
29
|
+
* sessionName: "WORKERS",
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* logger.log("REQUEST", { path: "/api/test", elapsed: 50 });
|
|
33
|
+
* // Output: [14:30:45.123] REQUEST path=/api/test elapsed=50
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createDebugFileLogger(config) {
|
|
37
|
+
const { filePath, envVar, sessionName, sessionHeader, width = 80 } = config;
|
|
38
|
+
let stream = null;
|
|
39
|
+
let initialized = false;
|
|
40
|
+
function isEnabled() {
|
|
41
|
+
// If no envVar specified, always enabled
|
|
42
|
+
if (!envVar)
|
|
43
|
+
return true;
|
|
44
|
+
return process.env[envVar] === "true";
|
|
45
|
+
}
|
|
46
|
+
function initLogFile() {
|
|
47
|
+
if (initialized)
|
|
48
|
+
return stream !== null;
|
|
49
|
+
initialized = true;
|
|
50
|
+
if (!isEnabled())
|
|
51
|
+
return false;
|
|
52
|
+
try {
|
|
53
|
+
const logDir = path.dirname(filePath);
|
|
54
|
+
if (!fs.existsSync(logDir)) {
|
|
55
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
stream = fs.createWriteStream(filePath, { flags: "w" });
|
|
58
|
+
activeLoggers.set(filePath, stream);
|
|
59
|
+
// Write session start marker
|
|
60
|
+
const header = sessionHeader ? ` | ${sessionHeader}` : "";
|
|
61
|
+
stream.write("\n" +
|
|
62
|
+
"=".repeat(width) +
|
|
63
|
+
"\n" +
|
|
64
|
+
`[${sessionName} SESSION START] ${new Date().toISOString()}${header}\n` +
|
|
65
|
+
"=".repeat(width) +
|
|
66
|
+
"\n");
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function formatTimestamp() {
|
|
74
|
+
return new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
|
|
75
|
+
}
|
|
76
|
+
function formatDetails(details) {
|
|
77
|
+
return Object.entries(details)
|
|
78
|
+
.filter(([_, v]) => v !== undefined)
|
|
79
|
+
.map(([k, v]) => {
|
|
80
|
+
if (typeof v === "object") {
|
|
81
|
+
return `${k}=${JSON.stringify(v)}`;
|
|
82
|
+
}
|
|
83
|
+
return `${k}=${v}`;
|
|
84
|
+
})
|
|
85
|
+
.join(" ");
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
log(action, details = {}) {
|
|
89
|
+
if (!initLogFile() || !stream)
|
|
90
|
+
return;
|
|
91
|
+
const timestamp = formatTimestamp();
|
|
92
|
+
const detailStr = formatDetails(details);
|
|
93
|
+
const line = `[${timestamp}] ${action.padEnd(20)} ${detailStr}\n`;
|
|
94
|
+
stream.write(line);
|
|
95
|
+
},
|
|
96
|
+
logRaw(line) {
|
|
97
|
+
if (!initLogFile() || !stream)
|
|
98
|
+
return;
|
|
99
|
+
stream.write(line.endsWith("\n") ? line : line + "\n");
|
|
100
|
+
},
|
|
101
|
+
isEnabled,
|
|
102
|
+
close(summary) {
|
|
103
|
+
if (!stream)
|
|
104
|
+
return;
|
|
105
|
+
const endMarker = "\n" +
|
|
106
|
+
"-".repeat(width) +
|
|
107
|
+
"\n" +
|
|
108
|
+
`[${sessionName} SESSION END] ${new Date().toISOString()}\n` +
|
|
109
|
+
(summary ? summary + "\n" : "") +
|
|
110
|
+
"-".repeat(width) +
|
|
111
|
+
"\n";
|
|
112
|
+
stream.write(endMarker);
|
|
113
|
+
stream.end();
|
|
114
|
+
activeLoggers.delete(filePath);
|
|
115
|
+
stream = null;
|
|
116
|
+
},
|
|
117
|
+
getStream() {
|
|
118
|
+
initLogFile();
|
|
119
|
+
return stream;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a logger that writes to both console and a file
|
|
125
|
+
* Always writes to console, writes to file when SST_DEBUG_BRIDGE is set
|
|
126
|
+
*/
|
|
127
|
+
export function createConsoleFileLogger(config) {
|
|
128
|
+
const { filePath, prefix, sessionName } = config;
|
|
129
|
+
const fileLogger = createDebugFileLogger({
|
|
130
|
+
filePath,
|
|
131
|
+
envVar: "SST_DEBUG_BRIDGE",
|
|
132
|
+
sessionName,
|
|
133
|
+
width: 100,
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
log(message) {
|
|
137
|
+
// Always log to console
|
|
138
|
+
console.log(`${prefix} ${message}`);
|
|
139
|
+
// Also log to file if enabled
|
|
140
|
+
if (fileLogger.isEnabled()) {
|
|
141
|
+
const timestamp = new Date().toISOString().slice(11, 23);
|
|
142
|
+
fileLogger.logRaw(`[${timestamp}] ${prefix} ${message}`);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
isEnabled: fileLogger.isEnabled,
|
|
146
|
+
close: () => fileLogger.close(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type EventType = "IOT_RECEIVED" | "IOT_ACK" | "WORKER_START" | "WORKER_LOG" | "WORKER_END";
|
|
2
|
+
interface EventTraceDetails {
|
|
3
|
+
requestID: string;
|
|
4
|
+
functionID?: string;
|
|
5
|
+
workerID?: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
status?: "success" | "error";
|
|
8
|
+
elapsed?: number;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Log an event trace entry with requestID as the common identifier
|
|
13
|
+
*
|
|
14
|
+
* @param event - The event type
|
|
15
|
+
* @param details - Event details including requestID (required)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* logEventTrace("IOT_RECEIVED", { requestID: "abc123", functionID: "MyFunc" });
|
|
19
|
+
* logEventTrace("WORKER_LOG", { requestID: "abc123", message: "[LOG] User action" });
|
|
20
|
+
* logEventTrace("WORKER_END", { requestID: "abc123", status: "success", elapsed: 150 });
|
|
21
|
+
*/
|
|
22
|
+
export declare function logEventTrace(event: EventType, details: EventTraceDetails): void;
|
|
23
|
+
/**
|
|
24
|
+
* Check if event trace logging is enabled
|
|
25
|
+
*/
|
|
26
|
+
export declare function isEventTraceEnabled(): boolean;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createDebugFileLogger } from "./debug-file-logger.js";
|
|
2
|
+
/**
|
|
3
|
+
* Event Trace Logging for SST Dev Bridge
|
|
4
|
+
*
|
|
5
|
+
* Tracks the complete lifecycle of a request from IOT reception to worker completion.
|
|
6
|
+
* All events share a common `requestID` for end-to-end tracing.
|
|
7
|
+
*
|
|
8
|
+
* Enable with: SST_EVENT_TRACE=true
|
|
9
|
+
*
|
|
10
|
+
* Log file: .sst/event-trace.log
|
|
11
|
+
*
|
|
12
|
+
* Event Types:
|
|
13
|
+
* - IOT_RECEIVED : IOT event received from Lambda
|
|
14
|
+
* - IOT_ACK : IOT acknowledgment published
|
|
15
|
+
* - WORKER_START : Worker function started
|
|
16
|
+
* - WORKER_LOG : Worker function log with [LOG] prefix
|
|
17
|
+
* - WORKER_END : Worker function completed (success/error)
|
|
18
|
+
*
|
|
19
|
+
* Tracing from Frontend:
|
|
20
|
+
* - Send X-Correlation-ID header from frontend for easy correlation
|
|
21
|
+
* - Or use the path to filter by endpoint
|
|
22
|
+
* - Or use apiGwReqId (visible in browser as x-amzn-requestid response header)
|
|
23
|
+
*/
|
|
24
|
+
const EVENT_TRACE_ENABLED = process.env.SST_EVENT_TRACE === "true";
|
|
25
|
+
let eventLogger = null;
|
|
26
|
+
function getEventLogger() {
|
|
27
|
+
if (!EVENT_TRACE_ENABLED)
|
|
28
|
+
return null;
|
|
29
|
+
if (!eventLogger) {
|
|
30
|
+
eventLogger = createDebugFileLogger({
|
|
31
|
+
filePath: ".sst/event-trace.log",
|
|
32
|
+
sessionName: "EVENT_TRACE",
|
|
33
|
+
width: 140,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return eventLogger;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Log an event trace entry with requestID as the common identifier
|
|
40
|
+
*
|
|
41
|
+
* @param event - The event type
|
|
42
|
+
* @param details - Event details including requestID (required)
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* logEventTrace("IOT_RECEIVED", { requestID: "abc123", functionID: "MyFunc" });
|
|
46
|
+
* logEventTrace("WORKER_LOG", { requestID: "abc123", message: "[LOG] User action" });
|
|
47
|
+
* logEventTrace("WORKER_END", { requestID: "abc123", status: "success", elapsed: 150 });
|
|
48
|
+
*/
|
|
49
|
+
export function logEventTrace(event, details) {
|
|
50
|
+
const logger = getEventLogger();
|
|
51
|
+
if (!logger)
|
|
52
|
+
return;
|
|
53
|
+
// Format requestID for consistent display (first 8 chars)
|
|
54
|
+
const reqId = details.requestID?.slice(0, 8) || "N/A";
|
|
55
|
+
// Build the log message with requestID prominently displayed
|
|
56
|
+
const logDetails = {
|
|
57
|
+
reqId,
|
|
58
|
+
...details,
|
|
59
|
+
};
|
|
60
|
+
// Remove the full requestID since we're using the shortened version
|
|
61
|
+
delete logDetails.requestID;
|
|
62
|
+
logger.log(event, logDetails);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if event trace logging is enabled
|
|
66
|
+
*/
|
|
67
|
+
export function isEventTraceEnabled() {
|
|
68
|
+
return EVENT_TRACE_ENABLED;
|
|
69
|
+
}
|
package/runtime/handlers/node.js
CHANGED
|
@@ -11,6 +11,7 @@ import { useRuntimeWorkers } from "../workers.js";
|
|
|
11
11
|
import { Colors } from "../../cli/colors.js";
|
|
12
12
|
import { Logger } from "../../logger.js";
|
|
13
13
|
import { findAbove } from "../../util/fs.js";
|
|
14
|
+
import { useMonoBuildConfig } from "../mono-build-config.js";
|
|
14
15
|
export const useNodeHandler = () => {
|
|
15
16
|
const rebuildCache = {};
|
|
16
17
|
process.on("exit", () => {
|
|
@@ -34,33 +35,72 @@ export const useNodeHandler = () => {
|
|
|
34
35
|
canHandle: (input) => input.startsWith("nodejs"),
|
|
35
36
|
startWorker: async (input) => {
|
|
36
37
|
const workers = await useRuntimeWorkers();
|
|
37
|
-
new
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
stdout: true,
|
|
48
|
-
});
|
|
49
|
-
worker.stdout.on("data", (data) => {
|
|
50
|
-
workers.stdout(input.workerID, data.toString());
|
|
51
|
-
});
|
|
52
|
-
worker.stderr.on("data", (data) => {
|
|
53
|
-
workers.stdout(input.workerID, data.toString());
|
|
54
|
-
});
|
|
55
|
-
worker.on("exit", () => workers.exited(input.workerID));
|
|
56
|
-
threads.set(input.workerID, worker);
|
|
38
|
+
const worker = new Worker(url.fileURLToPath(new URL("../../support/nodejs-runtime/index.mjs", import.meta.url)), {
|
|
39
|
+
env: {
|
|
40
|
+
...input.environment,
|
|
41
|
+
IS_LOCAL: "true",
|
|
42
|
+
},
|
|
43
|
+
execArgv: ["--enable-source-maps"],
|
|
44
|
+
workerData: input,
|
|
45
|
+
stderr: true,
|
|
46
|
+
stdin: true,
|
|
47
|
+
stdout: true,
|
|
57
48
|
});
|
|
49
|
+
worker.stdout.on("data", (data) => {
|
|
50
|
+
workers.stdout(input.workerID, data.toString());
|
|
51
|
+
});
|
|
52
|
+
worker.stderr.on("data", (data) => {
|
|
53
|
+
workers.stdout(input.workerID, data.toString());
|
|
54
|
+
});
|
|
55
|
+
worker.on("exit", () => workers.exited(input.workerID));
|
|
56
|
+
threads.set(input.workerID, worker);
|
|
58
57
|
},
|
|
59
58
|
stopWorker: async (workerID) => {
|
|
60
59
|
const worker = threads.get(workerID);
|
|
61
60
|
await worker?.terminate();
|
|
62
61
|
},
|
|
63
62
|
build: async (input) => {
|
|
63
|
+
// Check for dev mode mono-bundle using global config
|
|
64
|
+
const monoBuildConfig = useMonoBuildConfig();
|
|
65
|
+
if (input.mode === "start" && monoBuildConfig.enabled) {
|
|
66
|
+
Colors.line(Colors.prefix, Colors.dim.bold("MonoBundle"), Colors.dim(`mode=${input.mode}, handler=${input.props.handler}`));
|
|
67
|
+
// Symlink node_modules to mono-bundle dir for external dependencies
|
|
68
|
+
// Only create if symlink doesn't exist or points to wrong location (avoid redundant I/O)
|
|
69
|
+
const parsed = path.parse(input.props.handler);
|
|
70
|
+
const handlerFunctionsDir = path.join(project.paths.root, parsed.dir);
|
|
71
|
+
const root = await findAbove(handlerFunctionsDir, "package.json");
|
|
72
|
+
if (root) {
|
|
73
|
+
const sourceNodeModules = path.resolve(root, "node_modules");
|
|
74
|
+
const monoBundleNodeModules = path.join(monoBuildConfig.dir, "node_modules");
|
|
75
|
+
try {
|
|
76
|
+
const existingTarget = await fs.readlink(monoBundleNodeModules);
|
|
77
|
+
if (existingTarget === sourceNodeModules) {
|
|
78
|
+
// Symlink already correct, skip
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Symlink points to wrong location, recreate
|
|
82
|
+
await fs.rm(monoBundleNodeModules, { recursive: true, force: true });
|
|
83
|
+
await fs.symlink(sourceNodeModules, monoBundleNodeModules, "dir");
|
|
84
|
+
Logger.debug("Symlinked node_modules for mono-bundle from:", sourceNodeModules);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Symlink doesn't exist, create it
|
|
89
|
+
try {
|
|
90
|
+
await fs.symlink(sourceNodeModules, monoBundleNodeModules, "dir");
|
|
91
|
+
Logger.debug("Symlinked node_modules for mono-bundle from:", sourceNodeModules);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
Logger.debug("Failed to symlink node_modules for mono-bundle:", err);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
type: "success",
|
|
100
|
+
handler: monoBuildConfig.handler,
|
|
101
|
+
out: monoBuildConfig.dir,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
64
104
|
const parsed = path.parse(input.props.handler);
|
|
65
105
|
const file = [
|
|
66
106
|
".ts",
|
|
@@ -108,7 +148,8 @@ export const useNodeHandler = () => {
|
|
|
108
148
|
try {
|
|
109
149
|
await fs.symlink(path.resolve(dir), path.resolve(path.join(input.out, "node_modules")), "dir");
|
|
110
150
|
}
|
|
111
|
-
catch {
|
|
151
|
+
catch {
|
|
152
|
+
}
|
|
112
153
|
}
|
|
113
154
|
// Rebuilt using existing esbuild context
|
|
114
155
|
let ctx = rebuildCache[input.functionID]?.ctx;
|
package/runtime/handlers.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ declare module "../bus.js" {
|
|
|
6
6
|
};
|
|
7
7
|
"function.build.success": {
|
|
8
8
|
functionID: string;
|
|
9
|
+
monoBundle?: boolean;
|
|
9
10
|
};
|
|
10
11
|
"function.build.failed": {
|
|
11
12
|
functionID: string;
|
|
@@ -27,6 +28,8 @@ export interface StartWorkerInput {
|
|
|
27
28
|
out: string;
|
|
28
29
|
handler: string;
|
|
29
30
|
runtime: string;
|
|
31
|
+
/** Whether this worker is using mono build mode */
|
|
32
|
+
isMonoBuild?: boolean;
|
|
30
33
|
}
|
|
31
34
|
interface ShouldBuildInput {
|
|
32
35
|
file: string;
|
|
@@ -41,6 +44,7 @@ export interface RuntimeHandler {
|
|
|
41
44
|
type: "success";
|
|
42
45
|
handler: string;
|
|
43
46
|
sourcemap?: string;
|
|
47
|
+
out?: string;
|
|
44
48
|
} | {
|
|
45
49
|
type: "error";
|
|
46
50
|
errors: string[];
|
|
@@ -57,10 +61,10 @@ export declare const useRuntimeHandlers: () => {
|
|
|
57
61
|
type: "error";
|
|
58
62
|
errors: string[];
|
|
59
63
|
} | {
|
|
60
|
-
out: string;
|
|
61
|
-
sourcemap: string | undefined;
|
|
62
64
|
type: "success";
|
|
63
65
|
handler: string;
|
|
66
|
+
out: string;
|
|
67
|
+
sourcemap: string | undefined;
|
|
64
68
|
}>;
|
|
65
69
|
};
|
|
66
70
|
interface Artifact {
|
package/runtime/handlers.js
CHANGED
|
@@ -14,6 +14,9 @@ import { usePythonHandler } from "./handlers/python.js";
|
|
|
14
14
|
import { useRustHandler } from "./handlers/rust.js";
|
|
15
15
|
import { lazy } from "../util/lazy.js";
|
|
16
16
|
import { Semaphore } from "../util/semaphore.js";
|
|
17
|
+
import { isMonoBuildPath } from "./mono-build-config.js";
|
|
18
|
+
// Build concurrency semaphore - only used for actual builds, not mono-build lookups
|
|
19
|
+
const buildSemaphore = new Semaphore(parseInt(process.env.SST_BUILD_CONCURRENCY || "4", 10));
|
|
17
20
|
export const useRuntimeHandlers = lazy(() => {
|
|
18
21
|
const handlers = [
|
|
19
22
|
useNodeHandler(),
|
|
@@ -39,64 +42,110 @@ export const useRuntimeHandlers = lazy(() => {
|
|
|
39
42
|
return result;
|
|
40
43
|
},
|
|
41
44
|
async build(functionID, mode) {
|
|
42
|
-
|
|
45
|
+
// Fast path for mono-bundle: check without semaphore since no actual build work
|
|
46
|
+
async function tryMonoBundleFastPath() {
|
|
43
47
|
const func = useFunctions().fromID(functionID);
|
|
44
48
|
if (!func)
|
|
45
|
-
return
|
|
46
|
-
type: "error",
|
|
47
|
-
errors: [`Function with ID "${functionID}" not found`],
|
|
48
|
-
};
|
|
49
|
+
return null;
|
|
49
50
|
const handler = result.for(func.runtime);
|
|
50
51
|
const out = path.join(project.paths.artifacts, functionID);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (func.hooks?.beforeBuild)
|
|
55
|
-
await func.hooks.beforeBuild(func, out);
|
|
56
|
-
const built = await handler.build({
|
|
52
|
+
// Check for mono-bundle mode by doing a preliminary build call
|
|
53
|
+
// In mono-bundle mode, handler returns its own out path immediately without building
|
|
54
|
+
const monoBundleCheck = await handler.build({
|
|
57
55
|
functionID,
|
|
58
56
|
out,
|
|
59
57
|
mode,
|
|
60
58
|
props: func,
|
|
61
59
|
});
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// If mono-bundle detected (handler returned custom out), skip all artifact work
|
|
61
|
+
// Don't fire build events - mono-bundle is built externally by esbuild watch
|
|
62
|
+
// Worker pool invalidation is handled separately when bundle file actually changes
|
|
63
|
+
if (monoBundleCheck.type === "success" && monoBundleCheck.out) {
|
|
64
|
+
return {
|
|
65
|
+
type: "success",
|
|
66
|
+
handler: monoBundleCheck.handler,
|
|
67
|
+
out: monoBundleCheck.out,
|
|
68
|
+
sourcemap: monoBundleCheck.sourcemap,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return null; // Not mono-bundle, need full build
|
|
72
|
+
}
|
|
73
|
+
// Full build with semaphore protection for actual compilation work
|
|
74
|
+
async function fullBuild() {
|
|
75
|
+
const func = useFunctions().fromID(functionID);
|
|
76
|
+
if (!func)
|
|
77
|
+
return {
|
|
78
|
+
type: "error",
|
|
79
|
+
errors: [`Function with ID "${functionID}" not found`],
|
|
80
|
+
};
|
|
81
|
+
const handler = result.for(func.runtime);
|
|
82
|
+
const out = path.join(project.paths.artifacts, functionID);
|
|
83
|
+
// Acquire semaphore only for actual build work
|
|
84
|
+
const unlock = await buildSemaphore.lock();
|
|
85
|
+
try {
|
|
86
|
+
// Non-mono-bundle: follow original flow
|
|
87
|
+
await fs.rm(out, { recursive: true, force: true });
|
|
88
|
+
await fs.mkdir(out, { recursive: true });
|
|
89
|
+
bus.publish("function.build.started", { functionID });
|
|
90
|
+
if (func.hooks?.beforeBuild)
|
|
91
|
+
await func.hooks.beforeBuild(func, out);
|
|
92
|
+
const built = await handler.build({
|
|
64
93
|
functionID,
|
|
65
|
-
|
|
94
|
+
out,
|
|
95
|
+
mode,
|
|
96
|
+
props: func,
|
|
66
97
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
if (built.type === "error") {
|
|
99
|
+
bus.publish("function.build.failed", {
|
|
100
|
+
functionID,
|
|
101
|
+
errors: built.errors,
|
|
102
|
+
});
|
|
103
|
+
return built;
|
|
104
|
+
}
|
|
105
|
+
if (func.copyFiles) {
|
|
106
|
+
await Promise.all(func.copyFiles.map(async (entry) => {
|
|
107
|
+
const fromPath = path.join(project.paths.root, entry.from);
|
|
108
|
+
const to = entry.to || entry.from;
|
|
109
|
+
if (path.isAbsolute(to))
|
|
110
|
+
throw new Error(`Copy destination path "${to}" must be relative`);
|
|
111
|
+
const toPath = path.join(out, to);
|
|
112
|
+
if (mode === "deploy")
|
|
113
|
+
await fs.cp(fromPath, toPath, {
|
|
114
|
+
recursive: true,
|
|
115
|
+
});
|
|
116
|
+
if (mode === "start") {
|
|
117
|
+
try {
|
|
118
|
+
const dir = path.dirname(toPath);
|
|
119
|
+
await fs.mkdir(dir, { recursive: true });
|
|
120
|
+
await fs.symlink(fromPath, toPath);
|
|
121
|
+
}
|
|
122
|
+
catch (ex) {
|
|
123
|
+
Logger.debug("Failed to symlink", fromPath, toPath, ex);
|
|
124
|
+
}
|
|
85
125
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
})
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
if (func.hooks?.afterBuild)
|
|
129
|
+
await func.hooks.afterBuild(func, out);
|
|
130
|
+
bus.publish("function.build.success", { functionID });
|
|
131
|
+
return {
|
|
132
|
+
...built,
|
|
133
|
+
out,
|
|
134
|
+
sourcemap: built.sourcemap,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
unlock();
|
|
91
139
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
140
|
+
}
|
|
141
|
+
async function task() {
|
|
142
|
+
// Try mono-bundle fast path first (no semaphore needed)
|
|
143
|
+
const monoBundleResult = await tryMonoBundleFastPath();
|
|
144
|
+
if (monoBundleResult) {
|
|
145
|
+
return monoBundleResult;
|
|
146
|
+
}
|
|
147
|
+
// Fall back to full build with semaphore protection
|
|
148
|
+
return fullBuild();
|
|
100
149
|
}
|
|
101
150
|
if (pendingBuilds.has(functionID)) {
|
|
102
151
|
Logger.debug("Waiting on pending build", functionID);
|
|
@@ -118,27 +167,43 @@ export const useRuntimeHandlers = lazy(() => {
|
|
|
118
167
|
export const useFunctionBuilder = lazy(() => {
|
|
119
168
|
const artifacts = new Map();
|
|
120
169
|
const handlers = useRuntimeHandlers();
|
|
121
|
-
|
|
170
|
+
// Track pending builds to prevent duplicate concurrent builds for same function
|
|
171
|
+
const pendingArtifactBuilds = new Map();
|
|
122
172
|
const result = {
|
|
123
173
|
artifact: (functionID) => {
|
|
174
|
+
// Fast path: already cached - return immediately without any async work
|
|
124
175
|
if (artifacts.has(functionID))
|
|
125
176
|
return artifacts.get(functionID);
|
|
126
177
|
return result.build(functionID);
|
|
127
178
|
},
|
|
128
179
|
build: async (functionID) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const result = await handlers.build(functionID, "start");
|
|
132
|
-
if (!result)
|
|
133
|
-
return;
|
|
134
|
-
if (result.type === "error")
|
|
135
|
-
return;
|
|
136
|
-
artifacts.set(functionID, result);
|
|
180
|
+
// Fast path: already cached (check again in case of concurrent calls)
|
|
181
|
+
if (artifacts.has(functionID))
|
|
137
182
|
return artifacts.get(functionID);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
183
|
+
// Deduplication: if build already in progress for this function, wait for it
|
|
184
|
+
const pending = pendingArtifactBuilds.get(functionID);
|
|
185
|
+
if (pending)
|
|
186
|
+
return pending;
|
|
187
|
+
const buildTask = async () => {
|
|
188
|
+
try {
|
|
189
|
+
// handlers.build() handles semaphore internally:
|
|
190
|
+
// - mono-build: no semaphore (fast path)
|
|
191
|
+
// - non-mono-build: semaphore protected
|
|
192
|
+
const buildResult = await handlers.build(functionID, "start");
|
|
193
|
+
if (!buildResult)
|
|
194
|
+
return;
|
|
195
|
+
if (buildResult.type === "error")
|
|
196
|
+
return;
|
|
197
|
+
artifacts.set(functionID, buildResult);
|
|
198
|
+
return artifacts.get(functionID);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
pendingArtifactBuilds.delete(functionID);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const promise = buildTask();
|
|
205
|
+
pendingArtifactBuilds.set(functionID, promise);
|
|
206
|
+
return promise;
|
|
142
207
|
},
|
|
143
208
|
};
|
|
144
209
|
const watcher = useWatcher();
|
|
@@ -146,6 +211,11 @@ export const useFunctionBuilder = lazy(() => {
|
|
|
146
211
|
try {
|
|
147
212
|
const functions = useFunctions();
|
|
148
213
|
for (const [functionID, info] of Object.entries(functions.all)) {
|
|
214
|
+
// Optimization: For mono-build, the artifact path is stable and build is handled externally.
|
|
215
|
+
// We can skip the potentially expensive shouldBuild check and rebuild call.
|
|
216
|
+
const existing = artifacts.get(functionID);
|
|
217
|
+
if (existing && isMonoBuildPath(existing.out))
|
|
218
|
+
continue;
|
|
149
219
|
const handler = handlers.for(info.runtime);
|
|
150
220
|
if (!handler?.shouldBuild({
|
|
151
221
|
functionID,
|
|
@@ -156,7 +226,8 @@ export const useFunctionBuilder = lazy(() => {
|
|
|
156
226
|
Logger.debug("Rebuilt function", functionID);
|
|
157
227
|
}
|
|
158
228
|
}
|
|
159
|
-
catch {
|
|
229
|
+
catch {
|
|
230
|
+
}
|
|
160
231
|
});
|
|
161
232
|
return result;
|
|
162
233
|
});
|