in Clients/AmbrosiaJS/Ambrosia-Node/src/Meta.ts [2764:3456]
function emitConsumerTypeScriptFile(fileName: string, fileOptions: FileGenOptions, sourceFileName?: string): SourceFileProblemCheckResult | null
{
try
{
const NL: string = Utils.NEW_LINE; // Just for short-hand
const tab: string = " ".repeat(assertDefined(fileOptions.tabIndent));
let pathedOutputFile: string = Path.join(assertDefined(fileOptions.outputPath), `${Path.basename(fileName).replace(Path.extname(fileName), "")}.g.ts`);
let content: string = "";
let namespaces: string[] = [];
let namespaceComments: { [namespace: string]: string | undefined } = {}; // Effectively a rebuilt AST._namespaceJSDocComments but just for the namespaces that contain published entities
let previousNsPath: string = "";
checkForFileNameConflicts(pathedOutputFile, sourceFileName);
checkForGitMergeMarkers(pathedOutputFile);
// Note: We emit the types and methods using their original namespaces. If we didn't they'd all get emitted at the root level in the file (ie. no namespace) so we'd
// lose the original organizational structure of the code (which imparts the logical grouping/meaning of members). However, using namespaces is not required to prevent
// name collisions [in Ambrosia] because published types and methods must ALWAYS have unique names, even if defined in different TS namespaces.
// 1) Populate the 'namespaces' list
if (Object.keys(_publishedTypes).length > 0)
{
for (const typeName in _publishedTypes)
{
let type: Type = _publishedTypes[typeName];
if (type.codeGenOptions?.nsPath && (namespaces.indexOf(type.codeGenOptions.nsPath) === -1))
{
addNamespaces(type.codeGenOptions.nsPath);
namespaceComments[type.codeGenOptions.nsPath] = type.codeGenOptions.nsComment;
}
}
}
if (Object.keys(_publishedMethods).length > 0)
{
for (const name in _publishedMethods)
{
for (const version in _publishedMethods[name])
{
let method: Method = _publishedMethods[name][version];
if (method.codeGenOptions?.nsPath && (namespaces.indexOf(method.codeGenOptions.nsPath) === -1))
{
addNamespaces(method.codeGenOptions.nsPath);
namespaceComments[method.codeGenOptions.nsPath] = method.codeGenOptions.nsComment;
}
}
}
}
// 2) Emit types and methods
if (namespaces.length === 0) // The empty namespace (ie. no namespace)
{
// Emit types and methods at the root of the file
emitTypesAndMethods(0);
content = content.trimRight();
}
else
{
// Emit types and methods in their originating namespaces
namespaces.push(""); // We manually add the empty namespace (ie. no namespace) to emit entities which have no nsPath
namespaces = namespaces.sort(); // Eg. A, A.B, A.B.C, A.D, B, B.D, B.D.E, ...
for (const nsPath of namespaces)
{
const nsNestDepth: number = nsPath.split(".").length - 1;
const nsIndent: string = tab.repeat(nsNestDepth);
const nsName: string = nsPath ? nsPath.split(".")[nsNestDepth] : "";
const nsComment: string = namespaceComments[nsPath] ? AST.formatJSDocComment(assertDefined(namespaceComments[nsPath]), nsIndent.length) : "";
const previousNsNestDepth: number = previousNsPath.split(".").length - 1;
if (nsNestDepth > previousNsNestDepth)
{
// Start new nested namespace
if (nsComment)
{
content += nsComment + NL;
}
content += `${nsIndent}export namespace ${nsName}` + NL;
content += `${nsIndent}{` + NL;
}
else
{
if (previousNsPath)
{
// Emit closing braces (from the previous namespace depth back to the current depth)
content = content.trimRight() + NL;
for (let depth = previousNsNestDepth; depth >= nsNestDepth; depth--)
{
content += tab.repeat(depth) + "}" + NL;
}
content = content.trimRight() + NL.repeat(2);
}
if (nsPath)
{
if (nsComment)
{
content += nsComment + NL;
}
content += `${nsIndent}export namespace ${nsName}` + NL;
content += `${nsIndent}{` + NL;
}
}
emitTypesAndMethods(nsPath ? assertDefined(fileOptions.tabIndent) * (nsNestDepth + 1) : 0, nsPath);
previousNsPath = nsPath;
}
// Emit closing braces (back to the root)
content = content.trimRight() + NL;
for (let depth = previousNsPath.split(".").length - 1; depth >= 0; depth--)
{
content += tab.repeat(depth) + "}" + (depth !== 0 ? NL : "");
}
}
// 3) If there are published post methods, write a PostResultDispatcher().
// This is just a minimal outline with "TODO" placeholders where the developer should add their own code.
// Further, if the app includes multiple ConsumerInterface.g.ts files (because it uses more than one type of Ambrosia app/service), then the
// developer will need to unify the [potentially] multiple PostResultDispatcher's into a single dispatcher which can be passed to IC.start().
if (publishedPostMethodsExist())
{
let postResultDispatcher: string = "";
postResultDispatcher += "/**" + NL;
postResultDispatcher += " * Handler for the results of previously called post methods (in Ambrosia, only 'post' methods return values). See Messages.PostResultDispatcher.\\" + NL;
postResultDispatcher += " * Must return true only if the result (or error) was handled." + NL + " */" + NL;
postResultDispatcher += "export function postResultDispatcher(senderInstanceName: string, methodName: string, methodVersion: number, callID: number, callContextData: any, result: any, errorMsg: string): boolean" + NL;
postResultDispatcher += "{" + NL;
postResultDispatcher += tab + "const sender: string = IC.isSelf(senderInstanceName) ? \"local\" : `'${senderInstanceName}'`;" + NL;
postResultDispatcher += tab + "let handled: boolean = true;" + NL.repeat(2);
// We do this to help catch the case where the developer forgets to create a "unified" PostResultDispatcher [ie. that can
// handle post results for ALL used ConsumerInterface.g.ts files] so they end up accidentally re-using a PostResultDispatcher
// that's specific to just one type of Ambrosia app/service (and one destination [or set of destinations])
postResultDispatcher += tab + "if (_knownDestinations.indexOf(senderInstanceName) === -1)" + NL;
postResultDispatcher += tab + "{" + NL;
postResultDispatcher += tab + tab + `return (false); // Not handled: this post result is from a different instance than the destination instance currently (or previously) targeted by the '${fileOptions.apiName}' API` + NL;
postResultDispatcher += tab + "}" + NL.repeat(2);
postResultDispatcher += tab + "if (errorMsg)" + NL;
postResultDispatcher += tab + "{" + NL;
postResultDispatcher += tab + tab + "switch (methodName)" + NL;
postResultDispatcher += tab + tab + "{" + NL;
for (const name in _publishedMethods)
{
for (const version in _publishedMethods[name])
{
const method: Method = _publishedMethods[name][version];
if (method.isPost)
{
postResultDispatcher += tab.repeat(3) + `case \"${method.nameForTSWrapper}\":` + NL;
}
}
}
postResultDispatcher += tab.repeat(4) + "Utils.log(`Error: ${errorMsg}`);" + NL;
postResultDispatcher += tab.repeat(4) + "break;" + NL;
postResultDispatcher += tab.repeat(3) + "default:" + NL;
postResultDispatcher += tab.repeat(4) + "handled = false;" + NL;
postResultDispatcher += tab.repeat(4) + "break;" + NL;
postResultDispatcher += tab + tab + "}" + NL;
postResultDispatcher += tab + "}" + NL;
postResultDispatcher += tab + "else" + NL;
postResultDispatcher += tab + "{" + NL;
postResultDispatcher += tab + tab + "switch (methodName)" + NL;
postResultDispatcher += tab + tab + "{" + NL;
for (const name in _publishedMethods)
{
for (const version in _publishedMethods[name])
{
const method: Method = _publishedMethods[name][version];
if (method.isPost)
{
let nsPathOfReturnType: string = getPublishedType(Type.removeArraySuffix(method.returnType))?.codeGenOptions?.nsPath || "";
if (nsPathOfReturnType)
{
nsPathOfReturnType += ".";
}
postResultDispatcher += tab.repeat(3) + `case \"${method.nameForTSWrapper}\":` + NL;
if (method.returnType === "void")
{
postResultDispatcher += tab.repeat(4) + `// TODO: Handle the method completion (it returns void), optionally using the callContextData passed in the call` + NL;
}
else
{
postResultDispatcher += tab.repeat(4) + `const ${method.nameForTSWrapper}_Result: ${nsPathOfReturnType}${method.returnType} = result;` + NL;
postResultDispatcher += tab.repeat(4) + `// TODO: Handle the result, optionally using the callContextData passed in the call` + NL;
}
postResultDispatcher += tab.repeat(4) + "Utils.log(`Post method '${methodName}' from ${sender} IC succeeded`);" + NL;
postResultDispatcher += tab.repeat(4) + "break;" + NL;
}
}
}
postResultDispatcher += tab.repeat(3) + "default:" + NL;
postResultDispatcher += tab.repeat(4) + "handled = false;" + NL;
postResultDispatcher += tab.repeat(4) + "break;" + NL;
postResultDispatcher += tab + tab + "}" + NL;
postResultDispatcher += tab + "}" + NL;
postResultDispatcher += tab + "return (handled);" + NL;
postResultDispatcher += "}";
content += NL.repeat(2) + postResultDispatcher;
}
/** [Local function] Returns true if any published method is a post method. */
function publishedPostMethodsExist(): boolean
{
for (const name in _publishedMethods)
{
for (const version in _publishedMethods[name])
{
const method: Method = _publishedMethods[name][version];
if (method.isPost)
{
return (true);
}
}
}
return (false);
}
/** [Local function] Adds all the sub-paths for a given namespace path. */
function addNamespaces(nsPath: string): void
{
let nsSubPath: string = "";
for (let namespace of nsPath.split("."))
{
nsSubPath += ((nsSubPath.length > 0) ? "." : "") + namespace;
if (namespaces.indexOf(nsSubPath) === -1)
{
namespaces.push(nsSubPath);
}
}
}
/** [Local function] Adds published Types (as classes, type-definitions, or enum definitions) and published Methods (as function wrappers) to the 'content'. */
function emitTypesAndMethods(startingIndent: number, nsPath: string = ""): void
{
const pad: string = " ".repeat(startingIndent);
if (Object.keys(_publishedTypes).length > 0)
{
for (const typeName in _publishedTypes)
{
let type: Type = _publishedTypes[typeName];
if (type.codeGenOptions?.nsPath === nsPath)
{
content += type.makeTSType(startingIndent, fileOptions.tabIndent, type.codeGenOptions?.jsDocComment) + NL.repeat(2);
}
}
}
if (Object.keys(_publishedMethods).length > 0)
{
for (const name in _publishedMethods)
{
for (const version in _publishedMethods[name])
{
let method: Method = _publishedMethods[name][version];
if (method.codeGenOptions?.nsPath === nsPath)
{
content += method.makeTSWrappers(startingIndent, fileOptions, method.codeGenOptions?.jsDocComment) + NL.repeat(2);
}
}
}
}
}
if (content.length > 0)
{
let header: string = getHeaderCommentLines(GeneratedFileKind.Consumer, fileOptions).join(NL) + NL;
header += "import Ambrosia = require(\"ambrosia-node\");" + NL;
header += "import IC = Ambrosia.IC;" + NL;
header += "import Utils = Ambrosia.Utils;" + NL.repeat(2);
header += `const _knownDestinations: string[] = []; // All previously used destination instances (the '${fileOptions.apiName}' Ambrosia app/service can be running on multiple instances, potentially simultaneously); used by the postResultDispatcher (if any)` + NL;
header += "let _destinationInstanceName: string = \"\"; // The current destination instance" + NL;
header += "let _postTimeoutInMs: number = 8000; // -1 = Infinite" + NL.repeat(2);
header += "/** " + NL;
header += " * Sets the destination instance name that the API targets.\\" + NL;
header += ` * Must be called at least once (with the name of a registered Ambrosia instance that implements the '${fileOptions.apiName}' API) before any other method in the API is used.` + NL;
header += " */" + NL;
header += "export function setDestinationInstance(instanceName: string): void" + NL;
header += "{" + NL;
header += `${tab}_destinationInstanceName = instanceName.trim();` + NL;
header += `${tab}if (_destinationInstanceName && (_knownDestinations.indexOf(_destinationInstanceName) === -1))` + NL;
header += `${tab}{` + NL;
header += `${tab+tab}_knownDestinations.push(_destinationInstanceName);` + NL;
header += `${tab}}` + NL;
header += "}" + NL.repeat(2);
header += "/** Returns the destination instance name that the API currently targets. */" + NL;
header += "export function getDestinationInstance(): string" + NL;
header += "{" + NL;
header += `${tab}return (_destinationInstanceName);` + NL;
header += "}" + NL.repeat(2);
header += "/** Throws if _destinationInstanceName has not been set. */" + NL;
header += "function checkDestinationSet(): void" + NL;
header += "{" + NL;
header += `${tab}if (!_destinationInstanceName)` + NL;
header += `${tab}{` + NL;
header += `${tab+tab}throw new Error("setDestinationInstance() must be called to specify the target destination before the '${fileOptions.apiName}' API can be used.");` + NL;
header += `${tab}}` + NL;
header += "}" + NL.repeat(2);
header += "/**" + NL;
header += " * Sets the post method timeout interval (in milliseconds), which is how long to wait for a post result from the destination instance before raising an error.\\" + NL;
header += " * All post methods will use this timeout value. Specify -1 for no timeout. " + NL;
header += " */" + NL;
header += "export function setPostTimeoutInMs(timeoutInMs: number): void" + NL;
header += "{" + NL;
header += `${tab}_postTimeoutInMs = Math.max(-1, timeoutInMs);` + NL;
header += "}" + NL.repeat(2);
header += "/**" + NL;
header += " * Returns the post method timeout interval (in milliseconds), which is how long to wait for a post result from the destination instance before raising an error.\\" + NL;
header += " * A value of -1 means there is no timeout." + NL;
header += " */" + NL;
header += "export function getPostTimeoutInMs(): number" + NL;
header += "{" + NL;
header += `${tab}return (_postTimeoutInMs);` + NL;
header += "}" + NL.repeat(2);
content = header + content;
writeGeneratedFile(content, pathedOutputFile, assertDefined(fileOptions.mergeType));
Utils.log(`Code file generated: ${pathedOutputFile}${!fileOptions.checkGeneratedTS ? " (TypeScript checks skipped)" : ""}`);
return (fileOptions.checkGeneratedTS ? checkGeneratedFile(pathedOutputFile, (fileOptions.mergeType !== FileMergeType.None)) : new SourceFileProblemCheckResult());
}
else
{
throw new Error(sourceFileName ?
`The input source file (${Path.basename(sourceFileName)}) does not publish any entities (exported functions, static methods, type aliases and enums annotated with an ${CODEGEN_TAG_NAME} JSDoc tag)` :
"No entities have been published; call publishType() / publishMethod() / publishPostMethod() then retry");
}
}
catch (error: unknown)
{
Utils.log(`Error: emitConsumerTypeScriptFile() failed (reason: ${Utils.makeError(error).message})`);
return (null);
}
}
/**
* Writes the specified content to the specified pathedOutputFile, merging [using git] any existing changes in
* pathedOutputFile back into the newly generated file (according to mergeType). Throws if the merge fails.
* Returns the number of merge conflicts if 'mergeType' is FileMergeType.Annotate, or 0 otherwise.
*/
function writeGeneratedFile(content: string, pathedOutputFile: string, mergeType: FileMergeType): number
{
let conflictCount: number = 0;
let outputPath: string = Path.dirname(pathedOutputFile);
if ((mergeType === FileMergeType.None) || !File.existsSync(pathedOutputFile))
{
File.writeFileSync(pathedOutputFile, content); // This will overwrite the file if it already exists
}
else
{
// See https://stackoverflow.com/questions/9122948/run-git-merge-algorithm-on-two-individual-files
const pathedEmptyFile: string = Path.join(outputPath, "__empty.g.ts"); // This is a temporary file [but we don't want the name to collide with a real file]
const pathedRenamedOutputFile: string = Path.join(outputPath, "__" + Path.basename(pathedOutputFile) + ".original"); // This is a temporary file [but we don't want the name to collide with a real file]
try
{
// The output file already exists [possibly modified by the user], so merge the user's changes into new generated file
// using "git merge-file --union .\PublisherFramework.g.ts .\__empty.g.ts .\__PublisherFramework.g.ts.original". This will result in
// PublisherFramework.g.ts containing BOTH the new [generated] changes and the existing [user] changes (from .\__PublisherFramework.g.ts.original).
// Note: To just insert the merge markers that the developer will need to resolve manually, omit "--union" from "git merge-file"
// (ie. set mergeType to MergeType.Annotate).
Utils.log(`${FileMergeType[mergeType]}-merging existing ${pathedOutputFile} into generated version...`);
File.writeFileSync(pathedEmptyFile, ""); // The "common base" file
File.renameSync(pathedOutputFile, pathedRenamedOutputFile); // Save the original version (which may contain user edits); Will overwrite pathedRenamedOutputFile if it already exists
File.writeFileSync(pathedOutputFile, content); // This will overwrite the file
// Note: In launch.json, set '"autoAttachChildProcesses": false' to prevent the VS Code debugger from attaching to this process
// Note: See https://docs.npmjs.com/misc/config
let mergeOutput: string = ChildProcess.execSync(`git merge-file ${(mergeType === FileMergeType.Auto) ? "--union " : ""}-L Generated -L Base -L Original ${pathedOutputFile} ${pathedEmptyFile} ${pathedRenamedOutputFile}`, { encoding: "utf8", windowsHide: true, stdio: ["ignore"] }).trim();
if (mergeOutput)
{
throw (mergeOutput);
}
}
catch (error: unknown)
{
// Note: 'error' will be an Error object with some additional properties (output, pid, signal, status, stderr, stdout)
const err: Error = Utils.makeError(error);
// The 'git merge-file' exit code is negative on error, or the number of conflicts otherwise (truncated
// to 127 if there are more than that many conflicts); if the merge was clean, the exit value is 0
const gitExitCode: number = (err as any).status; // TODO: Hack to make compiler happy
if (gitExitCode >= 0) // 0 = clean merge, >0 = conflict count (typically this will only happen for a non-automatic merge, ie. if "--union" is omitted from "git merge-file")
{
conflictCount = gitExitCode;
}
else
{
// An error occurred, so restore the original version
File.renameSync(pathedRenamedOutputFile, pathedOutputFile);
const errorMsg: string = err.message.replace(/\s+/g, " ").trim().replace(/\.$/, "");
throw new Error(`Merge failed (reason: ${errorMsg} [exit code: ${gitExitCode}])`);
}
}
finally
{
// Remove temporary files
Utils.deleteFile(pathedEmptyFile);
Utils.deleteFile(pathedRenamedOutputFile);
}
// Note: Resolving merges requires that the "Editor: Code Lens" setting is enabled in VSCode
let userAction: string = (conflictCount === 0) ? "Please diff the changes to check merge correctness" : `Please manually resolve ${conflictCount} merge conflicts`;
Utils.logWithColor(Utils.ConsoleForegroundColors.Yellow, `${FileMergeType[mergeType]}-merge succeeded - ${userAction}`);
}
return (conflictCount);
}
/** Checks the generated 'pathedOutputFile' for TypeScript errors. Returns a SourceFileProblemCheckResult. */
function checkGeneratedFile(pathedOutputFile: string, mergeConflictMarkersAllowed: boolean = false): SourceFileProblemCheckResult
{
let result: SourceFileProblemCheckResult = AST.checkFileForTSProblems(pathedOutputFile, CodeGenerationFileType.Generated, mergeConflictMarkersAllowed);
if ((result.errorCount > 0))
{
Utils.log(`Error: TypeScript [${TS.version}] check failed for generated file ${Path.basename(pathedOutputFile)}: ${result.errorCount} error(s) found`);
}
else
{
Utils.log(`Success: No TypeScript errors found in generated file ${Path.basename(pathedOutputFile)}`);
}
return (result);
}
/** Returns the fully pathed version of the supplied TypeScript template file name [which is shipped in the ambrosia-node package], or throws an the file cannot be found. */
function getPathedTemplateFile(templateFileName: string): string
{
let pathedTemplateFile: string = "";
let searchFolders: string[] = [process.cwd(), Path.join(process.cwd(), "node_modules/ambrosia-node")]; // This only works if ambrosia-node has been installed locally (not globally, which we handle below)
for (const folder of searchFolders)
{
if (File.existsSync(Path.join(folder, templateFileName)))
{
pathedTemplateFile = Path.join(folder, templateFileName);
break;
}
}
if (pathedTemplateFile.length === 0)
{
// Last ditch (and costly) attempt, try to locate the global npm install location
try
{
// Note: In launch.json, set '"autoAttachChildProcesses": false' to prevent the VS Code debugger from attaching to this process
// Note: See https://docs.npmjs.com/misc/config
let globalNpmInstallFolder: string = ChildProcess.execSync("npm config get prefix", { encoding: "utf8", windowsHide: true }).trim();
// See https://docs.npmjs.com/files/folders
globalNpmInstallFolder = Path.join(globalNpmInstallFolder, Utils.isWindows() ? "" : "lib", "node_modules/ambrosia-node");
pathedTemplateFile = Path.join(globalNpmInstallFolder, templateFileName);
if (!File.existsSync(pathedTemplateFile))
{
searchFolders.push(globalNpmInstallFolder); // So that we can report where we tried to look for the [shipped] template
pathedTemplateFile = "";
}
}
catch (error: unknown)
{
const errorMsg: string = Utils.makeError(error).message.replace(/\s+/g, " ").trim().replace(/\.$/, "");
Utils.log(`Error: Unable to determine global npm install folder (reason: ${errorMsg})`);
}
if (pathedTemplateFile.length === 0)
{
throw new Error(`Unable to find template file ${templateFileName} in ${searchFolders.join(" or ")}`);
}
}
return (pathedTemplateFile);
}
/** The [TypeScript] alias used to reference the input source file in the generated PublisherFramework.g.ts file. */
const SOURCE_MODULE_ALIAS: string = "PTM"; // PTM = "Published Types and Methods"
/** Generates TypeScript code for the specified template section [of PublisherFramework.template.ts]. May return an empty string if there is no code for the section. */
function codeGen(section: CodeGenSection, fileOptions: FileGenOptions, sourceFileName?: string): string
{
const NL: string = Utils.NEW_LINE; // Just for short-hand
const tab: string = " ".repeat(assertDefined(fileOptions.tabIndent));
let lines: string[] = [];
let moduleAlias: string = sourceFileName ? SOURCE_MODULE_ALIAS + "." : "";
/** [Local function] Returns the TypeScript namespace (if any) of the supplied published type (if it exists), including the trailing ".". */
function makeParamTypePrefix(publishedType?: Type): string
{
if (publishedType)
{
if (publishedType.codeGenOptions?.nsPath)
{
return (moduleAlias + publishedType.codeGenOptions.nsPath + ".");
}
else
{
return (moduleAlias);
}
}
else
{
// No prefix required for a non-published type (eg. "string")
return ("");
}
}
// Skip this section if requested
if ((section & assertDefined(fileOptions.publisherSectionsToSkip)) === section)
{
return ("");
}
switch (section)
{
case CodeGenSection.Header:
lines.push(...getHeaderCommentLines(GeneratedFileKind.Publisher, fileOptions));
if (sourceFileName)
{
// Add an 'import' for the developer-provided source file that contains the implementations of published types and methods
// Note: We don't just want to use an absolute path to the source file (even though that would be simpler for us) because
// we want to retain any relative path so that it's easier for the user to move their code-base around.
// The reference to the source file must be relative to location of the generated file. For example, if the generated
// file is in ./src and the input source file is ./test/Foo.ts, then the import file reference should be '../test/Foo'
const relativeSourceFileName: string = Path.relative(assertDefined(fileOptions.outputPath), sourceFileName);
let filePath: string = Path.dirname(relativeSourceFileName.replace(/\\/g, "/"));
if ((filePath !== ".") && !filePath.startsWith("../") && !filePath.startsWith("./"))
{
filePath = "./" + filePath;
}
let fileReference: string = filePath + "/" + Path.basename(relativeSourceFileName).replace(Path.extname(relativeSourceFileName), "");
lines.push(`import * as ${SOURCE_MODULE_ALIAS} from "${fileReference}"; // ${SOURCE_MODULE_ALIAS} = "Published Types and Methods", but this file can also include app-state and app-event handlers`);
}
break;
case CodeGenSection.AppState:
if (sourceFileName)
{
if (AST.appStateVar() !== "")
{
lines.push(`// ${CODEGEN_COMMENT_PREFIX}: '${CodeGenSection[section]}' section skipped (using provided state variable '${SOURCE_MODULE_ALIAS}.${AST.appStateVar()}' and class '${AST.appStateVarClassName()}' instead)`);
break;
}
else
{
// Note: The _appState variable MUST be in an exported namespace (or module) so that it becomes a [mutable] property of an exported object [the namespace],
// thus allowing it to be set [by checkpointConsumer()] at runtime (see https://stackoverflow.com/questions/53617972/exported-variables-are-read-only).
// If it's exported from the root-level of the source file, the generated PublisherFramework.g.ts code will contain this error [in checkpointConsumer()]:
// Cannot assign to '_myAppState' because it is a read-only property. (ts:2540)
lines.push(`// TODO: It's recommended that you move this namespace to your input file (${makeRelativePath(assertDefined(fileOptions.outputPath), sourceFileName)}) then re-run code-gen`);
}
}
lines.push("export namespace State");
lines.push("{");
lines.push(tab + "export class AppState extends Ambrosia.AmbrosiaAppState" + NL + tab + "{");
lines.push(tab.repeat(2) + "// TODO: Define your application state here" + NL);
lines.push(tab.repeat(2) + "/**");
lines.push(tab.repeat(2) + " * @param restoredAppState Supplied only when loading (restoring) a checkpoint, or (for a \"VNext\" AppState) when upgrading from the prior AppState.\\");
lines.push(tab.repeat(2) + " * **WARNING:** When loading a checkpoint, restoredAppState will be an object literal, so you must use this to reinstantiate any members that are (or contain) class references.");
lines.push(tab.repeat(2) + " */");
lines.push(tab.repeat(2) + "constructor(restoredAppState?: AppState)");
lines.push(tab.repeat(2) + "{");
lines.push(tab.repeat(3) + "super(restoredAppState);" + NL);
lines.push(tab.repeat(3) + "if (restoredAppState)");
lines.push(tab.repeat(3) + "{");
lines.push(tab.repeat(4) + "// TODO: Re-initialize your application state from restoredAppState here");
lines.push(tab.repeat(4) + "// WARNING: You MUST reinstantiate all members that are (or contain) class references because restoredAppState is data-only");
lines.push(tab.repeat(3) + "}");
lines.push(tab.repeat(3) + "else");
lines.push(tab.repeat(3) + "{");
lines.push(tab.repeat(4) + "// TODO: Initialize your application state here");
lines.push(tab.repeat(3) + "}");
lines.push(tab.repeat(2) + "}" + NL + tab + "}" + NL);
lines.push(tab + "/**");
lines.push(tab + " * Only assign this using the return value of IC.start(), the return value of the upgrade() method of your AmbrosiaAppState");
lines.push(tab + " * instance, and [if not using the generated checkpointConsumer()] in the 'onFinished' callback of an IncomingCheckpoint object.");
lines.push(tab + " */");
lines.push(tab + "export let _appState: AppState;");
lines.push("}")
break;
case CodeGenSection.PostMethodHandlers:
for (const name in _publishedMethods)
{
for (const version in _publishedMethods[name])
{
let method: Method = _publishedMethods[name][version];
let variableNames: string[] = method.parameterNames.map(name => name.endsWith("?") ? name.slice(0, -1) : name);
let nsPathForMethod: string = method.codeGenOptions?.nsPath ? (method.codeGenOptions.nsPath + ".") : "";
if (method.isPost)
{
let caseTab: string = tab;
lines.push(`case "${method.name}":`);
if (variableNames.length > 0)
{
// To prevent variable name collisions in the switch statement (eg. if 2 methods use the same parameter name), if needed, we create a new block scope for each case statement
lines.push(`${tab}{`);
caseTab = tab + tab;
}
for (let i = 0; i < variableNames.length; i++)
{
let prefix: string = makeParamTypePrefix(_publishedTypes[Type.removeArraySuffix(method.parameterTypes[i])]);
lines.push(`${caseTab}const ${Method.trimRest(variableNames[i])}: ${prefix}${method.parameterTypes[i]} = IC.getPostMethodArg(rpc, "${Method.trimRest(method.parameterNames[i])}");`);
}
let prefix: string = makeParamTypePrefix(_publishedTypes[Type.removeArraySuffix(method.returnType)]);
lines.push(`${caseTab}IC.postResult<${prefix}${method.returnType}>(rpc, ${moduleAlias + nsPathForMethod}${method.name}(${variableNames.join(", ")}));`);
if (variableNames.length > 0)
{
lines.push(`${tab}}`);
}
lines.push(`${tab}break;${NL}`);
}
}
}
break;
case CodeGenSection.NonPostMethodHandlers:
for (const name in _publishedMethods)
{
for (const version in _publishedMethods[name])
{
let method: Method = _publishedMethods[name][version];
let variableNames: string[] = method.parameterNames.map(name => name.endsWith("?") ? name.slice(0, -1) : name);
let nsPathForMethod: string = method.codeGenOptions?.nsPath ? (method.codeGenOptions.nsPath + ".") : "";
if (!method.isPost)
{
let caseTab: string = tab;
lines.push(`case ${method.id}:`);
if (variableNames.length > 0)
{
// To prevent variable name collisions in the switch statement (eg. if 2 methods use the same parameter name), if needed, we create a new block scope for each case statement
lines.push(`${tab}{`);
caseTab = tab + tab;
}
for (let i = 0; i < variableNames.length; i++)
{
let prefix: string = makeParamTypePrefix(_publishedTypes[Type.removeArraySuffix(method.parameterTypes[i])]);
if (method.takesRawParams)
{
lines.push(`${caseTab}const ${variableNames[i]}: ${prefix}${method.parameterTypes[i]} = rpc.getRawParams();`);
}
else
{
const isOptionalParam: boolean = method.parameterNames[i].endsWith("?");
const paramName: string = isOptionalParam ? method.parameterNames[i].slice(0, -1) : method.parameterNames[i];
lines.push(`${caseTab}const ${Method.trimRest(variableNames[i])}: ${prefix}${method.parameterTypes[i]} = rpc.getJsonParam("${Method.trimRest(paramName)}");${isOptionalParam ? " // Optional parameter" : ""}`);
}
}
lines.push(`${caseTab}${moduleAlias + nsPathForMethod}${method.name}(${variableNames.join(", ")});`);
if (variableNames.length > 0)
{
lines.push(`${tab}}`);
}
lines.push(`${tab}break;${NL}`);
}
}
}
break;
case CodeGenSection.PublishTypes:
for (const typeName in _publishedTypes)
{
let type: Type = _publishedTypes[typeName];
lines.push(`Meta.publishType("${type.name}", "${type.definition.replace(/"/g, "\\\"")}");`);
}
break;
case CodeGenSection.PublishMethods:
for (const name in _publishedMethods)
{
for (const version in _publishedMethods[name])
{
let method: Method = _publishedMethods[name][version];
let paramList: string[] = [];
paramList.push(...method.parameterNames.map((name, index) => `"${name}: ${method.parameterTypes[index].replace(/"/g, "\\\"")}"`));
let methodParams: string = `[${paramList.join(", ")}]`;
if (method.isPost)
{
lines.push(`Meta.publishPostMethod("${method.name}", ${method.version}, ${methodParams}, "${method.returnType.replace(/"/g, "\\\"")}"${method.isTypeChecked ? "" : ", false"});`);
}
else
{
lines.push(`Meta.publishMethod(${method.id}, "${method.name}", ${methodParams});`);
}
}
}