@milaboratories/pl-middle-layer 1.48.16 → 1.48.18

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.
@@ -1 +1 @@
1
- {"version":3,"file":"network_check.cjs","names":["createTempFile","createBigTempFile","backendPings","blockRegistryOverviewPings","blockGARegistryOverviewPings","blockRegistryUiPings","blockGARegistryUiPings","autoUpdateCdnPings","uploadTemplate","uploadFile","downloadFile","softwareCheck","pythonSoftware","downloadFromEveryStorage","UnauthenticatedPlClient","PlClient","ConsoleLoggerAdapter","HmacSha256Signer","LsDriver","reportToString"],"sources":["../../src/network_check/network_check.ts"],"sourcesContent":["/** A utility to check network problems and gather statistics.\n * It's useful when we cannot connect to the server of a company\n * because of security reasons,\n * but they can send us and their DevOps team this report.\n *\n * What we check:\n * - pings to backend\n * - block registry for block overview and ui.\n * - autoupdate CDN.\n * - upload workflow to backend (workflow part via our API).\n * - the desktop could do multipart upload.\n * - the desktop could download files from S3.\n * - backend could download software and run it.\n * - backend could run python software.\n * - try to get something from every storage to work storage.\n *\n * We don't check backend access to S3 storage, it is checked on the start of backend.\n */\n\nimport type { AuthInformation, PlClientConfig } from \"@milaboratories/pl-client\";\nimport { PlClient, UnauthenticatedPlClient, plAddressToConfig } from \"@milaboratories/pl-client\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { ConsoleLoggerAdapter, HmacSha256Signer } from \"@milaboratories/ts-helpers\";\nimport { channel } from \"node:diagnostics_channel\";\nimport type { ClientDownload, ClientUpload } from \"@milaboratories/pl-drivers\";\nimport { LsDriver, createDownloadClient, createUploadBlobClient } from \"@milaboratories/pl-drivers\";\nimport type { HttpNetworkReport, NetworkReport } from \"./pings\";\nimport {\n autoUpdateCdnPings,\n backendPings,\n blockGARegistryOverviewPings,\n blockGARegistryUiPings,\n blockRegistryOverviewPings,\n blockRegistryUiPings,\n reportToString,\n} from \"./pings\";\nimport type { Dispatcher } from \"undici\";\nimport type { TemplateReport } from \"./template\";\nimport {\n uploadTemplate,\n uploadFile,\n downloadFile,\n createTempFile,\n pythonSoftware,\n softwareCheck,\n createBigTempFile,\n downloadFromEveryStorage,\n} from \"./template\";\nimport { randomUUID } from \"node:crypto\";\n\n/** All reports we need to collect. */\ninterface NetworkReports {\n plPings: NetworkReport<string>[];\n\n blockRegistryOverviewChecks: HttpNetworkReport[];\n blockGARegistryOverviewChecks: HttpNetworkReport[];\n blockRegistryUiChecks: HttpNetworkReport[];\n blockGARegistryUiChecks: HttpNetworkReport[];\n\n autoUpdateCdnChecks: HttpNetworkReport[];\n\n uploadTemplateCheck: TemplateReport;\n uploadFileCheck: TemplateReport;\n downloadFileCheck: TemplateReport;\n softwareCheck: TemplateReport;\n pythonSoftwareCheck: TemplateReport;\n storageToDownloadReport: Record<string, TemplateReport>;\n}\n\nexport interface CheckNetworkOpts {\n /** Platforma Backend pings options. */\n pingCheckDurationMs: number;\n pingTimeoutMs: number;\n maxPingsPerSecond: number;\n\n /** An options for CDN and block registry. */\n httpTimeoutMs: number;\n\n /** Block registry pings options. */\n blockRegistryDurationMs: number;\n maxRegistryChecksPerSecond: number;\n blockRegistryUrl: string;\n blockGARegistryUrl: string;\n blockOverviewPath: string;\n blockUiPath: string;\n\n /** CDN for auto-update pings options. */\n autoUpdateCdnDurationMs: number;\n maxAutoUpdateCdnChecksPerSecond: number;\n autoUpdateCdnUrl: string;\n\n /** Body limit for requests. */\n bodyLimit: number;\n\n /** Limit for the size of files to download from every storage. */\n everyStorageBytesLimit: number;\n /** Minimal size of files to create a directory from for every storage. */\n everyStorageMinFileSize: number;\n /** Maximal size of files to create a directory from for every storage. */\n everyStorageMaxFileSize: number;\n /** How many files to check from every storage. */\n everyStorageNFilesToCheck: number;\n /** Minimal number of ls requests for every storage. */\n everyStorageMinLsRequests: number;\n}\n\n/** Checks connectivity to Platforma Backend, to block registry\n * and to auto-update CDN,\n * and generates a string report. */\nexport async function checkNetwork(\n plCredentials: string,\n plUser: string | undefined,\n plPassword: string | undefined,\n optsOverrides: Partial<CheckNetworkOpts> = {},\n): Promise<string> {\n const undiciLogs: any[] = [];\n // Subscribe to all Undici diagnostic events\n undiciEvents.forEach((event) => {\n const diagnosticChannel = channel(event);\n diagnosticChannel.subscribe((message: any) => {\n const timestamp = new Date().toISOString();\n const data = { ...message };\n if (data?.response?.headers) {\n data.response = { ...data.response };\n data.response.headers = data.response.headers.slice();\n data.response.headers = data.response.headers.map((h: any) => h.toString());\n }\n\n // we try to upload big files, don't include the buffer in the report.\n if (data?.request?.body) {\n data.request = { ...data.request };\n data.request.body = `too big`;\n }\n\n undiciLogs.push(\n JSON.stringify({\n timestamp,\n event,\n data,\n }),\n );\n });\n });\n\n try {\n const {\n logger,\n plConfig,\n client,\n signer,\n downloadClient,\n uploadBlobClient,\n lsDriver,\n httpClient,\n ops,\n } = await initNetworkCheck(plCredentials, plUser, plPassword, optsOverrides);\n\n const { filePath: filePathToDownload, fileContent: fileContentToDownload } =\n await createTempFile();\n const { filePath: filePathToUpload } = await createBigTempFile();\n\n const report: NetworkReports = {\n plPings: await backendPings(ops, plConfig),\n blockRegistryOverviewChecks: await blockRegistryOverviewPings(ops, httpClient),\n blockGARegistryOverviewChecks: await blockGARegistryOverviewPings(ops, httpClient),\n blockRegistryUiChecks: await blockRegistryUiPings(ops, httpClient),\n blockGARegistryUiChecks: await blockGARegistryUiPings(ops, httpClient),\n\n autoUpdateCdnChecks: await autoUpdateCdnPings(ops, httpClient),\n\n uploadTemplateCheck: await uploadTemplate(logger, client, \"Jack\"),\n uploadFileCheck: await uploadFile(\n logger,\n signer,\n lsDriver,\n uploadBlobClient,\n client,\n filePathToUpload,\n ),\n downloadFileCheck: await downloadFile(\n logger,\n client,\n lsDriver,\n uploadBlobClient,\n downloadClient,\n filePathToDownload,\n fileContentToDownload,\n ),\n softwareCheck: await softwareCheck(client),\n pythonSoftwareCheck: await pythonSoftware(client, \"Jack\"),\n storageToDownloadReport: await downloadFromEveryStorage(logger, client, lsDriver, {\n minLsRequests: ops.everyStorageMinLsRequests,\n bytesLimit: ops.everyStorageBytesLimit,\n minFileSize: ops.everyStorageMinFileSize,\n maxFileSize: ops.everyStorageMaxFileSize,\n nFilesToCheck: ops.everyStorageNFilesToCheck,\n }),\n };\n\n return reportsToString(report, plCredentials, ops, undiciLogs);\n } catch (e) {\n return `Unhandled error while checking the network: ${e}`;\n }\n}\n\nexport async function initNetworkCheck(\n plCredentials: string,\n plUser: string | undefined,\n plPassword: string | undefined,\n optsOverrides: Partial<CheckNetworkOpts> = {},\n): Promise<{\n logger: MiLogger;\n plConfig: PlClientConfig;\n signer: Signer;\n client: PlClient;\n downloadClient: ClientDownload;\n uploadBlobClient: ClientUpload;\n lsDriver: LsDriver;\n httpClient: Dispatcher;\n ops: CheckNetworkOpts;\n terminate: () => Promise<void>;\n}> {\n const ops: CheckNetworkOpts = {\n pingCheckDurationMs: 10000,\n pingTimeoutMs: 3000,\n maxPingsPerSecond: 50,\n\n httpTimeoutMs: 3000,\n\n blockRegistryDurationMs: 3000,\n maxRegistryChecksPerSecond: 1,\n\n blockRegistryUrl: \"https://blocks.pl-open.science\",\n blockGARegistryUrl: \"https://blocks-ga.pl-open.science\",\n blockOverviewPath: \"v2/overview.json\",\n blockUiPath: \"v2/milaboratories/samples-and-data/1.7.0/ui.tgz\",\n\n autoUpdateCdnDurationMs: 5000,\n maxAutoUpdateCdnChecksPerSecond: 1,\n autoUpdateCdnUrl:\n \"https://cdn.platforma.bio/software/platforma-desktop-v2/windows/amd64/latest.yml\",\n\n bodyLimit: 300,\n\n everyStorageBytesLimit: 1024,\n everyStorageMinFileSize: 1024,\n everyStorageMaxFileSize: 200 * 1024 * 1024, // 200 MB\n everyStorageNFilesToCheck: 300,\n everyStorageMinLsRequests: 50,\n ...optsOverrides,\n };\n\n const plConfig = plAddressToConfig(plCredentials, {\n defaultRequestTimeout: ops.pingTimeoutMs,\n });\n\n // exposing alternative root for fields not to interfere with\n // projects of the user.\n plConfig.alternativeRoot = `check_network_${randomUUID()}`;\n\n const uaClient = await UnauthenticatedPlClient.build(plConfig);\n\n let auth: AuthInformation = {};\n if (plUser && plPassword) {\n auth = await uaClient.login(plUser, plPassword);\n }\n\n const client = await PlClient.init(plCredentials, { authInformation: auth });\n\n const httpClient = uaClient.ll.httpDispatcher;\n const logger = new ConsoleLoggerAdapter();\n\n // FIXME: do we need to get an actual secret?\n const signer = new HmacSha256Signer(\"localSecret\");\n\n // We could initialize middle-layer here, but for now it seems like an overkill.\n // Here's the code to do it:\n //\n // const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'platforma-network-check-'));\n // const ml = await MiddleLayer.init(client, tmpDir, {\n // logger,\n // localSecret: '',\n // localProjections: [],\n // openFileDialogCallback: () => Promise.resolve([]),\n // preferredUpdateChannel: 'stable',\n // });\n\n const downloadClient = createDownloadClient(logger, client, []);\n const uploadBlobClient = createUploadBlobClient(client, logger);\n\n const lsDriver = await LsDriver.init(logger, client, signer, [], () => Promise.resolve([]), []);\n\n const terminate = async () => {\n downloadClient.close();\n uploadBlobClient.close();\n await httpClient.close();\n await client.close();\n };\n\n return {\n logger,\n plConfig,\n client,\n signer,\n downloadClient,\n uploadBlobClient,\n lsDriver,\n httpClient,\n ops,\n terminate,\n };\n}\n\nfunction reportsToString(\n report: NetworkReports,\n plEndpoint: string,\n opts: CheckNetworkOpts,\n undiciLogs: any[],\n): string {\n const successPings = report.plPings.filter((p) => p.response.ok);\n const failedPings = report.plPings.filter((p) => !p.response.ok);\n const successPingsBodies = [\n ...new Set(successPings.map((p) => JSON.stringify((p.response as any).value))),\n ];\n\n const summary = (ok: boolean) => (ok ? \"OK\" : \"FAILED\");\n const templateSummary = (report: TemplateReport) =>\n report.status === \"ok\" ? \"OK\" : report.status === \"warn\" ? \"WARN\" : \"FAILED\";\n\n const pings = reportToString(report.plPings);\n const blockRegistryOverview = reportToString(report.blockRegistryOverviewChecks);\n const blockGARegistryOverview = reportToString(report.blockGARegistryOverviewChecks);\n const blockRegistryUi = reportToString(report.blockRegistryUiChecks);\n const blockGARegistryUi = reportToString(report.blockGARegistryUiChecks);\n const autoUpdateCdn = reportToString(report.autoUpdateCdnChecks);\n\n const storagesSummary = Object.entries(report.storageToDownloadReport)\n .map(([storage, report]) => `${templateSummary(report)} ${storage} storage check`)\n .join(\"\\n\");\n\n return `\n${summary(pings.ok)} pings to Platforma Backend\n${summary(blockRegistryOverview.ok)} block registry overview\n${summary(blockGARegistryOverview.ok)} block ga registry overview\n${summary(blockRegistryUi.ok)} block registry ui\n${summary(blockGARegistryUi.ok)} block ga registry ui\n${summary(autoUpdateCdn.ok)} auto-update CDN\n${templateSummary(report.uploadTemplateCheck)} upload template\n${templateSummary(report.uploadFileCheck)} upload file\n${templateSummary(report.downloadFileCheck)} download file\n${templateSummary(report.softwareCheck)} software check\n${templateSummary(report.pythonSoftwareCheck)} python software check\n${storagesSummary}\n\ndetails:\npl endpoint: ${plEndpoint};\noptions: ${JSON.stringify(opts, null, 2)}.\n\nUpload template response: ${report.uploadTemplateCheck.message}\n\nUpload file response: ${report.uploadFileCheck.message}\n\nDownload file response: ${report.downloadFileCheck.message}\n\nSoftware check response: ${report.softwareCheck.message}\nPython software check response: ${report.pythonSoftwareCheck.message}\nStorage to download responses: ${JSON.stringify(report.storageToDownloadReport, null, 2)}\n\nPlatforma pings: ${pings.details}\n\nBlock registry overview responses: ${blockRegistryOverview.details}\n\nBlock ga registry overview responses: ${blockGARegistryOverview.details}\n\nBlock registry ui responses: ${blockRegistryUi.details}\n\nBlock ga registry ui responses: ${blockGARegistryUi.details}\n\nAuto-update CDN responses: ${autoUpdateCdn.details}\n\ndumps:\nBlock registry overview dumps:\n${JSON.stringify(report.blockRegistryOverviewChecks, null, 2)}\n\nBlock ga registry overview dumps:\n${JSON.stringify(report.blockGARegistryOverviewChecks, null, 2)}\n\nBlock registry ui dumps:\n${JSON.stringify(report.blockRegistryUiChecks, null, 2)}\n\nBlock ga registry ui dumps:\n${JSON.stringify(report.blockGARegistryUiChecks, null, 2)}\n\nAuto-update CDN dumps:\n${JSON.stringify(report.autoUpdateCdnChecks, null, 2)}\n\nPlatforma pings error dumps:\n${JSON.stringify(failedPings, null, 2)}\n\nPlatforma pings success dump examples:\n${JSON.stringify(successPingsBodies, null, 2)}\n\nUndici logs:\n${undiciLogs.join(\"\\n\")}\n`;\n}\n\n// List of Undici diagnostic channels\nconst undiciEvents: string[] = [\n \"undici:request:create\", // When a new request is created\n \"undici:request:bodySent\", // When the request body is sent\n \"undici:request:headers\", // When request headers are sent\n \"undici:request:error\", // When a request encounters an error\n \"undici:request:trailers\", // When a response completes.\n\n \"undici:client:sendHeaders\",\n \"undici:client:beforeConnect\",\n \"undici:client:connected\",\n \"undici:client:connectError\",\n\n \"undici:socket:close\", // When a socket is closed\n \"undici:socket:connect\", // When a socket connects\n \"undici:socket:error\", // When a socket encounters an error\n\n \"undici:pool:request\", // When a request is added to the pool\n \"undici:pool:connect\", // When a pool creates a new connection\n \"undici:pool:disconnect\", // When a pool connection is closed\n \"undici:pool:destroy\", // When a pool is destroyed\n \"undici:dispatcher:request\", // When a dispatcher processes a request\n \"undici:dispatcher:connect\", // When a dispatcher connects\n \"undici:dispatcher:disconnect\", // When a dispatcher disconnects\n \"undici:dispatcher:retry\", // When a dispatcher retries a request\n];\n"],"mappings":"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"}
1
+ {"version":3,"file":"network_check.cjs","names":["createTempFile","createBigTempFile","backendPings","blockRegistryOverviewPings","blockGARegistryOverviewPings","blockRegistryUiPings","blockGARegistryUiPings","autoUpdateCdnPings","uploadTemplate","uploadFile","downloadFile","softwareCheck","pythonSoftware","downloadFromEveryStorage","UnauthenticatedPlClient","PlClient","ConsoleLoggerAdapter","HmacSha256Signer","LsDriver","reportToString"],"sources":["../../src/network_check/network_check.ts"],"sourcesContent":["/** A utility to check network problems and gather statistics.\n * It's useful when we cannot connect to the server of a company\n * because of security reasons,\n * but they can send us and their DevOps team this report.\n *\n * What we check:\n * - pings to backend\n * - block registry for block overview and ui.\n * - autoupdate CDN.\n * - upload workflow to backend (workflow part via our API).\n * - the desktop could do multipart upload.\n * - the desktop could download files from S3.\n * - backend could download software and run it.\n * - backend could run python software.\n * - try to get something from every storage to work storage.\n *\n * We don't check backend access to S3 storage, it is checked on the start of backend.\n */\n\nimport type { AuthInformation, PlClientConfig } from \"@milaboratories/pl-client\";\nimport { PlClient, UnauthenticatedPlClient, plAddressToConfig } from \"@milaboratories/pl-client\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { ConsoleLoggerAdapter, HmacSha256Signer } from \"@milaboratories/ts-helpers\";\nimport { channel } from \"node:diagnostics_channel\";\nimport type { ClientDownload, ClientUpload } from \"@milaboratories/pl-drivers\";\nimport { LsDriver, createDownloadClient, createUploadBlobClient } from \"@milaboratories/pl-drivers\";\nimport type { HttpNetworkReport, NetworkReport } from \"./pings\";\nimport {\n autoUpdateCdnPings,\n backendPings,\n blockGARegistryOverviewPings,\n blockGARegistryUiPings,\n blockRegistryOverviewPings,\n blockRegistryUiPings,\n reportToString,\n} from \"./pings\";\nimport type { Dispatcher } from \"undici\";\nimport type { TemplateReport } from \"./template\";\nimport {\n uploadTemplate,\n uploadFile,\n downloadFile,\n createTempFile,\n pythonSoftware,\n softwareCheck,\n createBigTempFile,\n downloadFromEveryStorage,\n} from \"./template\";\nimport { randomUUID } from \"node:crypto\";\n\n/** All reports we need to collect. */\ninterface NetworkReports {\n plPings: NetworkReport<string>[];\n\n blockRegistryOverviewChecks: HttpNetworkReport[];\n blockGARegistryOverviewChecks: HttpNetworkReport[];\n blockRegistryUiChecks: HttpNetworkReport[];\n blockGARegistryUiChecks: HttpNetworkReport[];\n\n autoUpdateCdnChecks: HttpNetworkReport[];\n\n uploadTemplateCheck: TemplateReport;\n uploadFileCheck: TemplateReport;\n downloadFileCheck: TemplateReport;\n softwareCheck: TemplateReport;\n pythonSoftwareCheck: TemplateReport;\n storageToDownloadReport: Record<string, TemplateReport>;\n}\n\nexport interface CheckNetworkOpts {\n /** Signal to abort all network checks. */\n signal?: AbortSignal;\n\n /** Platforma Backend pings options. */\n pingCheckDurationMs: number;\n pingTimeoutMs: number;\n maxPingsPerSecond: number;\n\n /** An options for CDN and block registry. */\n httpTimeoutMs: number;\n\n /** Block registry pings options. */\n blockRegistryDurationMs: number;\n maxRegistryChecksPerSecond: number;\n blockRegistryUrl: string;\n blockGARegistryUrl: string;\n blockOverviewPath: string;\n blockUiPath: string;\n\n /** CDN for auto-update pings options. */\n autoUpdateCdnDurationMs: number;\n maxAutoUpdateCdnChecksPerSecond: number;\n autoUpdateCdnUrl: string;\n\n /** Body limit for requests. */\n bodyLimit: number;\n\n /** Limit for the size of files to download from every storage. */\n everyStorageBytesLimit: number;\n /** Minimal size of files to create a directory from for every storage. */\n everyStorageMinFileSize: number;\n /** Maximal size of files to create a directory from for every storage. */\n everyStorageMaxFileSize: number;\n /** How many files to check from every storage. */\n everyStorageNFilesToCheck: number;\n /** Minimal number of ls requests for every storage. */\n everyStorageMinLsRequests: number;\n}\n\n/** Checks connectivity to Platforma Backend, to block registry\n * and to auto-update CDN,\n * and generates a string report. */\nexport async function checkNetwork(\n plCredentials: string,\n plUser: string | undefined,\n plPassword: string | undefined,\n optsOverrides: Partial<CheckNetworkOpts> = {},\n): Promise<string> {\n const undiciLogs: any[] = [];\n // Subscribe to all Undici diagnostic events\n undiciEvents.forEach((event) => {\n const diagnosticChannel = channel(event);\n diagnosticChannel.subscribe((message: any) => {\n const timestamp = new Date().toISOString();\n const data = { ...message };\n if (data?.response?.headers) {\n data.response = { ...data.response };\n data.response.headers = data.response.headers.slice();\n data.response.headers = data.response.headers.map((h: any) => h.toString());\n }\n\n // we try to upload big files, don't include the buffer in the report.\n if (data?.request?.body) {\n data.request = { ...data.request };\n data.request.body = `too big`;\n }\n\n undiciLogs.push(\n JSON.stringify({\n timestamp,\n event,\n data,\n }),\n );\n });\n });\n\n try {\n const {\n logger,\n plConfig,\n client,\n signer,\n downloadClient,\n uploadBlobClient,\n lsDriver,\n httpClient,\n ops,\n } = await initNetworkCheck(plCredentials, plUser, plPassword, optsOverrides);\n\n const { filePath: filePathToDownload, fileContent: fileContentToDownload } =\n await createTempFile();\n const { filePath: filePathToUpload } = await createBigTempFile();\n\n const report: NetworkReports = {\n plPings: await backendPings(ops, plConfig),\n blockRegistryOverviewChecks: await blockRegistryOverviewPings(ops, httpClient),\n blockGARegistryOverviewChecks: await blockGARegistryOverviewPings(ops, httpClient),\n blockRegistryUiChecks: await blockRegistryUiPings(ops, httpClient),\n blockGARegistryUiChecks: await blockGARegistryUiPings(ops, httpClient),\n\n autoUpdateCdnChecks: await autoUpdateCdnPings(ops, httpClient),\n\n uploadTemplateCheck: await uploadTemplate(logger, client, \"Jack\"),\n uploadFileCheck: await uploadFile(\n logger,\n signer,\n lsDriver,\n uploadBlobClient,\n client,\n filePathToUpload,\n ),\n downloadFileCheck: await downloadFile(\n logger,\n client,\n lsDriver,\n uploadBlobClient,\n downloadClient,\n filePathToDownload,\n fileContentToDownload,\n ),\n softwareCheck: await softwareCheck(client),\n pythonSoftwareCheck: await pythonSoftware(client, \"Jack\"),\n storageToDownloadReport: await downloadFromEveryStorage(logger, client, lsDriver, {\n minLsRequests: ops.everyStorageMinLsRequests,\n bytesLimit: ops.everyStorageBytesLimit,\n minFileSize: ops.everyStorageMinFileSize,\n maxFileSize: ops.everyStorageMaxFileSize,\n nFilesToCheck: ops.everyStorageNFilesToCheck,\n }),\n };\n\n return reportsToString(report, plCredentials, ops, undiciLogs);\n } catch (e) {\n return `Unhandled error while checking the network: ${e}`;\n }\n}\n\nexport async function initNetworkCheck(\n plCredentials: string,\n plUser: string | undefined,\n plPassword: string | undefined,\n optsOverrides: Partial<CheckNetworkOpts> = {},\n): Promise<{\n logger: MiLogger;\n plConfig: PlClientConfig;\n signer: Signer;\n client: PlClient;\n downloadClient: ClientDownload;\n uploadBlobClient: ClientUpload;\n lsDriver: LsDriver;\n httpClient: Dispatcher;\n ops: CheckNetworkOpts;\n terminate: () => Promise<void>;\n}> {\n const ops: CheckNetworkOpts = {\n pingCheckDurationMs: 10000,\n pingTimeoutMs: 3000,\n maxPingsPerSecond: 50,\n\n httpTimeoutMs: 3000,\n\n blockRegistryDurationMs: 3000,\n maxRegistryChecksPerSecond: 1,\n\n blockRegistryUrl: \"https://blocks.pl-open.science\",\n blockGARegistryUrl: \"https://blocks-ga.pl-open.science\",\n blockOverviewPath: \"v2/overview.json\",\n blockUiPath: \"v2/milaboratories/samples-and-data/1.7.0/ui.tgz\",\n\n autoUpdateCdnDurationMs: 5000,\n maxAutoUpdateCdnChecksPerSecond: 1,\n autoUpdateCdnUrl:\n \"https://cdn.platforma.bio/software/platforma-desktop-v2/windows/amd64/latest.yml\",\n\n bodyLimit: 300,\n\n everyStorageBytesLimit: 1024,\n everyStorageMinFileSize: 1024,\n everyStorageMaxFileSize: 200 * 1024 * 1024, // 200 MB\n everyStorageNFilesToCheck: 300,\n everyStorageMinLsRequests: 50,\n ...optsOverrides,\n };\n\n const plConfig = plAddressToConfig(plCredentials, {\n defaultRequestTimeout: ops.pingTimeoutMs,\n });\n\n // exposing alternative root for fields not to interfere with\n // projects of the user.\n plConfig.alternativeRoot = `check_network_${randomUUID()}`;\n\n const uaClient = await UnauthenticatedPlClient.build(plConfig);\n\n let auth: AuthInformation = {};\n if (plUser && plPassword) {\n auth = await uaClient.login(plUser, plPassword);\n }\n\n const client = await PlClient.init(plCredentials, { authInformation: auth });\n\n const httpClient = uaClient.ll.httpDispatcher;\n const logger = new ConsoleLoggerAdapter();\n\n // FIXME: do we need to get an actual secret?\n const signer = new HmacSha256Signer(\"localSecret\");\n\n // We could initialize middle-layer here, but for now it seems like an overkill.\n // Here's the code to do it:\n //\n // const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'platforma-network-check-'));\n // const ml = await MiddleLayer.init(client, tmpDir, {\n // logger,\n // localSecret: '',\n // localProjections: [],\n // openFileDialogCallback: () => Promise.resolve([]),\n // preferredUpdateChannel: 'stable',\n // });\n\n const downloadClient = createDownloadClient(logger, client, []);\n const uploadBlobClient = createUploadBlobClient(client, logger);\n\n const lsDriver = await LsDriver.init(logger, client, signer, [], () => Promise.resolve([]), []);\n\n const terminate = async () => {\n downloadClient.close();\n uploadBlobClient.close();\n await httpClient.close();\n await client.close();\n };\n\n return {\n logger,\n plConfig,\n client,\n signer,\n downloadClient,\n uploadBlobClient,\n lsDriver,\n httpClient,\n ops,\n terminate,\n };\n}\n\nfunction reportsToString(\n report: NetworkReports,\n plEndpoint: string,\n opts: CheckNetworkOpts,\n undiciLogs: any[],\n): string {\n const successPings = report.plPings.filter((p) => p.response.ok);\n const failedPings = report.plPings.filter((p) => !p.response.ok);\n const successPingsBodies = [\n ...new Set(successPings.map((p) => JSON.stringify((p.response as any).value))),\n ];\n\n const summary = (ok: boolean) => (ok ? \"OK\" : \"FAILED\");\n const templateSummary = (report: TemplateReport) =>\n report.status === \"ok\" ? \"OK\" : report.status === \"warn\" ? \"WARN\" : \"FAILED\";\n\n const pings = reportToString(report.plPings);\n const blockRegistryOverview = reportToString(report.blockRegistryOverviewChecks);\n const blockGARegistryOverview = reportToString(report.blockGARegistryOverviewChecks);\n const blockRegistryUi = reportToString(report.blockRegistryUiChecks);\n const blockGARegistryUi = reportToString(report.blockGARegistryUiChecks);\n const autoUpdateCdn = reportToString(report.autoUpdateCdnChecks);\n\n const storagesSummary = Object.entries(report.storageToDownloadReport)\n .map(([storage, report]) => `${templateSummary(report)} ${storage} storage check`)\n .join(\"\\n\");\n\n return `\n${summary(pings.ok)} pings to Platforma Backend\n${summary(blockRegistryOverview.ok)} block registry overview\n${summary(blockGARegistryOverview.ok)} block ga registry overview\n${summary(blockRegistryUi.ok)} block registry ui\n${summary(blockGARegistryUi.ok)} block ga registry ui\n${summary(autoUpdateCdn.ok)} auto-update CDN\n${templateSummary(report.uploadTemplateCheck)} upload template\n${templateSummary(report.uploadFileCheck)} upload file\n${templateSummary(report.downloadFileCheck)} download file\n${templateSummary(report.softwareCheck)} software check\n${templateSummary(report.pythonSoftwareCheck)} python software check\n${storagesSummary}\n\ndetails:\npl endpoint: ${plEndpoint};\noptions: ${JSON.stringify(opts, null, 2)}.\n\nUpload template response: ${report.uploadTemplateCheck.message}\n\nUpload file response: ${report.uploadFileCheck.message}\n\nDownload file response: ${report.downloadFileCheck.message}\n\nSoftware check response: ${report.softwareCheck.message}\nPython software check response: ${report.pythonSoftwareCheck.message}\nStorage to download responses: ${JSON.stringify(report.storageToDownloadReport, null, 2)}\n\nPlatforma pings: ${pings.details}\n\nBlock registry overview responses: ${blockRegistryOverview.details}\n\nBlock ga registry overview responses: ${blockGARegistryOverview.details}\n\nBlock registry ui responses: ${blockRegistryUi.details}\n\nBlock ga registry ui responses: ${blockGARegistryUi.details}\n\nAuto-update CDN responses: ${autoUpdateCdn.details}\n\ndumps:\nBlock registry overview dumps:\n${JSON.stringify(report.blockRegistryOverviewChecks, null, 2)}\n\nBlock ga registry overview dumps:\n${JSON.stringify(report.blockGARegistryOverviewChecks, null, 2)}\n\nBlock registry ui dumps:\n${JSON.stringify(report.blockRegistryUiChecks, null, 2)}\n\nBlock ga registry ui dumps:\n${JSON.stringify(report.blockGARegistryUiChecks, null, 2)}\n\nAuto-update CDN dumps:\n${JSON.stringify(report.autoUpdateCdnChecks, null, 2)}\n\nPlatforma pings error dumps:\n${JSON.stringify(failedPings, null, 2)}\n\nPlatforma pings success dump examples:\n${JSON.stringify(successPingsBodies, null, 2)}\n\nUndici logs:\n${undiciLogs.join(\"\\n\")}\n`;\n}\n\n// List of Undici diagnostic channels\nconst undiciEvents: string[] = [\n \"undici:request:create\", // When a new request is created\n \"undici:request:bodySent\", // When the request body is sent\n \"undici:request:headers\", // When request headers are sent\n \"undici:request:error\", // When a request encounters an error\n \"undici:request:trailers\", // When a response completes.\n\n \"undici:client:sendHeaders\",\n \"undici:client:beforeConnect\",\n \"undici:client:connected\",\n \"undici:client:connectError\",\n\n \"undici:socket:close\", // When a socket is closed\n \"undici:socket:connect\", // When a socket connects\n \"undici:socket:error\", // When a socket encounters an error\n\n \"undici:pool:request\", // When a request is added to the pool\n \"undici:pool:connect\", // When a pool creates a new connection\n \"undici:pool:disconnect\", // When a pool connection is closed\n \"undici:pool:destroy\", // When a pool is destroyed\n \"undici:dispatcher:request\", // When a dispatcher processes a request\n \"undici:dispatcher:connect\", // When a dispatcher connects\n \"undici:dispatcher:disconnect\", // When a dispatcher disconnects\n \"undici:dispatcher:retry\", // When a dispatcher retries a request\n];\n"],"mappings":"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"}
@@ -5,6 +5,8 @@ import { ClientDownload, ClientUpload, LsDriver } from "@milaboratories/pl-drive
5
5
 
6
6
  //#region src/network_check/network_check.d.ts
7
7
  interface CheckNetworkOpts {
8
+ /** Signal to abort all network checks. */
9
+ signal?: AbortSignal;
8
10
  /** Platforma Backend pings options. */
9
11
  pingCheckDurationMs: number;
10
12
  pingTimeoutMs: number;
@@ -1 +1 @@
1
- {"version":3,"file":"network_check.js","names":[],"sources":["../../src/network_check/network_check.ts"],"sourcesContent":["/** A utility to check network problems and gather statistics.\n * It's useful when we cannot connect to the server of a company\n * because of security reasons,\n * but they can send us and their DevOps team this report.\n *\n * What we check:\n * - pings to backend\n * - block registry for block overview and ui.\n * - autoupdate CDN.\n * - upload workflow to backend (workflow part via our API).\n * - the desktop could do multipart upload.\n * - the desktop could download files from S3.\n * - backend could download software and run it.\n * - backend could run python software.\n * - try to get something from every storage to work storage.\n *\n * We don't check backend access to S3 storage, it is checked on the start of backend.\n */\n\nimport type { AuthInformation, PlClientConfig } from \"@milaboratories/pl-client\";\nimport { PlClient, UnauthenticatedPlClient, plAddressToConfig } from \"@milaboratories/pl-client\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { ConsoleLoggerAdapter, HmacSha256Signer } from \"@milaboratories/ts-helpers\";\nimport { channel } from \"node:diagnostics_channel\";\nimport type { ClientDownload, ClientUpload } from \"@milaboratories/pl-drivers\";\nimport { LsDriver, createDownloadClient, createUploadBlobClient } from \"@milaboratories/pl-drivers\";\nimport type { HttpNetworkReport, NetworkReport } from \"./pings\";\nimport {\n autoUpdateCdnPings,\n backendPings,\n blockGARegistryOverviewPings,\n blockGARegistryUiPings,\n blockRegistryOverviewPings,\n blockRegistryUiPings,\n reportToString,\n} from \"./pings\";\nimport type { Dispatcher } from \"undici\";\nimport type { TemplateReport } from \"./template\";\nimport {\n uploadTemplate,\n uploadFile,\n downloadFile,\n createTempFile,\n pythonSoftware,\n softwareCheck,\n createBigTempFile,\n downloadFromEveryStorage,\n} from \"./template\";\nimport { randomUUID } from \"node:crypto\";\n\n/** All reports we need to collect. */\ninterface NetworkReports {\n plPings: NetworkReport<string>[];\n\n blockRegistryOverviewChecks: HttpNetworkReport[];\n blockGARegistryOverviewChecks: HttpNetworkReport[];\n blockRegistryUiChecks: HttpNetworkReport[];\n blockGARegistryUiChecks: HttpNetworkReport[];\n\n autoUpdateCdnChecks: HttpNetworkReport[];\n\n uploadTemplateCheck: TemplateReport;\n uploadFileCheck: TemplateReport;\n downloadFileCheck: TemplateReport;\n softwareCheck: TemplateReport;\n pythonSoftwareCheck: TemplateReport;\n storageToDownloadReport: Record<string, TemplateReport>;\n}\n\nexport interface CheckNetworkOpts {\n /** Platforma Backend pings options. */\n pingCheckDurationMs: number;\n pingTimeoutMs: number;\n maxPingsPerSecond: number;\n\n /** An options for CDN and block registry. */\n httpTimeoutMs: number;\n\n /** Block registry pings options. */\n blockRegistryDurationMs: number;\n maxRegistryChecksPerSecond: number;\n blockRegistryUrl: string;\n blockGARegistryUrl: string;\n blockOverviewPath: string;\n blockUiPath: string;\n\n /** CDN for auto-update pings options. */\n autoUpdateCdnDurationMs: number;\n maxAutoUpdateCdnChecksPerSecond: number;\n autoUpdateCdnUrl: string;\n\n /** Body limit for requests. */\n bodyLimit: number;\n\n /** Limit for the size of files to download from every storage. */\n everyStorageBytesLimit: number;\n /** Minimal size of files to create a directory from for every storage. */\n everyStorageMinFileSize: number;\n /** Maximal size of files to create a directory from for every storage. */\n everyStorageMaxFileSize: number;\n /** How many files to check from every storage. */\n everyStorageNFilesToCheck: number;\n /** Minimal number of ls requests for every storage. */\n everyStorageMinLsRequests: number;\n}\n\n/** Checks connectivity to Platforma Backend, to block registry\n * and to auto-update CDN,\n * and generates a string report. */\nexport async function checkNetwork(\n plCredentials: string,\n plUser: string | undefined,\n plPassword: string | undefined,\n optsOverrides: Partial<CheckNetworkOpts> = {},\n): Promise<string> {\n const undiciLogs: any[] = [];\n // Subscribe to all Undici diagnostic events\n undiciEvents.forEach((event) => {\n const diagnosticChannel = channel(event);\n diagnosticChannel.subscribe((message: any) => {\n const timestamp = new Date().toISOString();\n const data = { ...message };\n if (data?.response?.headers) {\n data.response = { ...data.response };\n data.response.headers = data.response.headers.slice();\n data.response.headers = data.response.headers.map((h: any) => h.toString());\n }\n\n // we try to upload big files, don't include the buffer in the report.\n if (data?.request?.body) {\n data.request = { ...data.request };\n data.request.body = `too big`;\n }\n\n undiciLogs.push(\n JSON.stringify({\n timestamp,\n event,\n data,\n }),\n );\n });\n });\n\n try {\n const {\n logger,\n plConfig,\n client,\n signer,\n downloadClient,\n uploadBlobClient,\n lsDriver,\n httpClient,\n ops,\n } = await initNetworkCheck(plCredentials, plUser, plPassword, optsOverrides);\n\n const { filePath: filePathToDownload, fileContent: fileContentToDownload } =\n await createTempFile();\n const { filePath: filePathToUpload } = await createBigTempFile();\n\n const report: NetworkReports = {\n plPings: await backendPings(ops, plConfig),\n blockRegistryOverviewChecks: await blockRegistryOverviewPings(ops, httpClient),\n blockGARegistryOverviewChecks: await blockGARegistryOverviewPings(ops, httpClient),\n blockRegistryUiChecks: await blockRegistryUiPings(ops, httpClient),\n blockGARegistryUiChecks: await blockGARegistryUiPings(ops, httpClient),\n\n autoUpdateCdnChecks: await autoUpdateCdnPings(ops, httpClient),\n\n uploadTemplateCheck: await uploadTemplate(logger, client, \"Jack\"),\n uploadFileCheck: await uploadFile(\n logger,\n signer,\n lsDriver,\n uploadBlobClient,\n client,\n filePathToUpload,\n ),\n downloadFileCheck: await downloadFile(\n logger,\n client,\n lsDriver,\n uploadBlobClient,\n downloadClient,\n filePathToDownload,\n fileContentToDownload,\n ),\n softwareCheck: await softwareCheck(client),\n pythonSoftwareCheck: await pythonSoftware(client, \"Jack\"),\n storageToDownloadReport: await downloadFromEveryStorage(logger, client, lsDriver, {\n minLsRequests: ops.everyStorageMinLsRequests,\n bytesLimit: ops.everyStorageBytesLimit,\n minFileSize: ops.everyStorageMinFileSize,\n maxFileSize: ops.everyStorageMaxFileSize,\n nFilesToCheck: ops.everyStorageNFilesToCheck,\n }),\n };\n\n return reportsToString(report, plCredentials, ops, undiciLogs);\n } catch (e) {\n return `Unhandled error while checking the network: ${e}`;\n }\n}\n\nexport async function initNetworkCheck(\n plCredentials: string,\n plUser: string | undefined,\n plPassword: string | undefined,\n optsOverrides: Partial<CheckNetworkOpts> = {},\n): Promise<{\n logger: MiLogger;\n plConfig: PlClientConfig;\n signer: Signer;\n client: PlClient;\n downloadClient: ClientDownload;\n uploadBlobClient: ClientUpload;\n lsDriver: LsDriver;\n httpClient: Dispatcher;\n ops: CheckNetworkOpts;\n terminate: () => Promise<void>;\n}> {\n const ops: CheckNetworkOpts = {\n pingCheckDurationMs: 10000,\n pingTimeoutMs: 3000,\n maxPingsPerSecond: 50,\n\n httpTimeoutMs: 3000,\n\n blockRegistryDurationMs: 3000,\n maxRegistryChecksPerSecond: 1,\n\n blockRegistryUrl: \"https://blocks.pl-open.science\",\n blockGARegistryUrl: \"https://blocks-ga.pl-open.science\",\n blockOverviewPath: \"v2/overview.json\",\n blockUiPath: \"v2/milaboratories/samples-and-data/1.7.0/ui.tgz\",\n\n autoUpdateCdnDurationMs: 5000,\n maxAutoUpdateCdnChecksPerSecond: 1,\n autoUpdateCdnUrl:\n \"https://cdn.platforma.bio/software/platforma-desktop-v2/windows/amd64/latest.yml\",\n\n bodyLimit: 300,\n\n everyStorageBytesLimit: 1024,\n everyStorageMinFileSize: 1024,\n everyStorageMaxFileSize: 200 * 1024 * 1024, // 200 MB\n everyStorageNFilesToCheck: 300,\n everyStorageMinLsRequests: 50,\n ...optsOverrides,\n };\n\n const plConfig = plAddressToConfig(plCredentials, {\n defaultRequestTimeout: ops.pingTimeoutMs,\n });\n\n // exposing alternative root for fields not to interfere with\n // projects of the user.\n plConfig.alternativeRoot = `check_network_${randomUUID()}`;\n\n const uaClient = await UnauthenticatedPlClient.build(plConfig);\n\n let auth: AuthInformation = {};\n if (plUser && plPassword) {\n auth = await uaClient.login(plUser, plPassword);\n }\n\n const client = await PlClient.init(plCredentials, { authInformation: auth });\n\n const httpClient = uaClient.ll.httpDispatcher;\n const logger = new ConsoleLoggerAdapter();\n\n // FIXME: do we need to get an actual secret?\n const signer = new HmacSha256Signer(\"localSecret\");\n\n // We could initialize middle-layer here, but for now it seems like an overkill.\n // Here's the code to do it:\n //\n // const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'platforma-network-check-'));\n // const ml = await MiddleLayer.init(client, tmpDir, {\n // logger,\n // localSecret: '',\n // localProjections: [],\n // openFileDialogCallback: () => Promise.resolve([]),\n // preferredUpdateChannel: 'stable',\n // });\n\n const downloadClient = createDownloadClient(logger, client, []);\n const uploadBlobClient = createUploadBlobClient(client, logger);\n\n const lsDriver = await LsDriver.init(logger, client, signer, [], () => Promise.resolve([]), []);\n\n const terminate = async () => {\n downloadClient.close();\n uploadBlobClient.close();\n await httpClient.close();\n await client.close();\n };\n\n return {\n logger,\n plConfig,\n client,\n signer,\n downloadClient,\n uploadBlobClient,\n lsDriver,\n httpClient,\n ops,\n terminate,\n };\n}\n\nfunction reportsToString(\n report: NetworkReports,\n plEndpoint: string,\n opts: CheckNetworkOpts,\n undiciLogs: any[],\n): string {\n const successPings = report.plPings.filter((p) => p.response.ok);\n const failedPings = report.plPings.filter((p) => !p.response.ok);\n const successPingsBodies = [\n ...new Set(successPings.map((p) => JSON.stringify((p.response as any).value))),\n ];\n\n const summary = (ok: boolean) => (ok ? \"OK\" : \"FAILED\");\n const templateSummary = (report: TemplateReport) =>\n report.status === \"ok\" ? \"OK\" : report.status === \"warn\" ? \"WARN\" : \"FAILED\";\n\n const pings = reportToString(report.plPings);\n const blockRegistryOverview = reportToString(report.blockRegistryOverviewChecks);\n const blockGARegistryOverview = reportToString(report.blockGARegistryOverviewChecks);\n const blockRegistryUi = reportToString(report.blockRegistryUiChecks);\n const blockGARegistryUi = reportToString(report.blockGARegistryUiChecks);\n const autoUpdateCdn = reportToString(report.autoUpdateCdnChecks);\n\n const storagesSummary = Object.entries(report.storageToDownloadReport)\n .map(([storage, report]) => `${templateSummary(report)} ${storage} storage check`)\n .join(\"\\n\");\n\n return `\n${summary(pings.ok)} pings to Platforma Backend\n${summary(blockRegistryOverview.ok)} block registry overview\n${summary(blockGARegistryOverview.ok)} block ga registry overview\n${summary(blockRegistryUi.ok)} block registry ui\n${summary(blockGARegistryUi.ok)} block ga registry ui\n${summary(autoUpdateCdn.ok)} auto-update CDN\n${templateSummary(report.uploadTemplateCheck)} upload template\n${templateSummary(report.uploadFileCheck)} upload file\n${templateSummary(report.downloadFileCheck)} download file\n${templateSummary(report.softwareCheck)} software check\n${templateSummary(report.pythonSoftwareCheck)} python software check\n${storagesSummary}\n\ndetails:\npl endpoint: ${plEndpoint};\noptions: ${JSON.stringify(opts, null, 2)}.\n\nUpload template response: ${report.uploadTemplateCheck.message}\n\nUpload file response: ${report.uploadFileCheck.message}\n\nDownload file response: ${report.downloadFileCheck.message}\n\nSoftware check response: ${report.softwareCheck.message}\nPython software check response: ${report.pythonSoftwareCheck.message}\nStorage to download responses: ${JSON.stringify(report.storageToDownloadReport, null, 2)}\n\nPlatforma pings: ${pings.details}\n\nBlock registry overview responses: ${blockRegistryOverview.details}\n\nBlock ga registry overview responses: ${blockGARegistryOverview.details}\n\nBlock registry ui responses: ${blockRegistryUi.details}\n\nBlock ga registry ui responses: ${blockGARegistryUi.details}\n\nAuto-update CDN responses: ${autoUpdateCdn.details}\n\ndumps:\nBlock registry overview dumps:\n${JSON.stringify(report.blockRegistryOverviewChecks, null, 2)}\n\nBlock ga registry overview dumps:\n${JSON.stringify(report.blockGARegistryOverviewChecks, null, 2)}\n\nBlock registry ui dumps:\n${JSON.stringify(report.blockRegistryUiChecks, null, 2)}\n\nBlock ga registry ui dumps:\n${JSON.stringify(report.blockGARegistryUiChecks, null, 2)}\n\nAuto-update CDN dumps:\n${JSON.stringify(report.autoUpdateCdnChecks, null, 2)}\n\nPlatforma pings error dumps:\n${JSON.stringify(failedPings, null, 2)}\n\nPlatforma pings success dump examples:\n${JSON.stringify(successPingsBodies, null, 2)}\n\nUndici logs:\n${undiciLogs.join(\"\\n\")}\n`;\n}\n\n// List of Undici diagnostic channels\nconst undiciEvents: string[] = [\n \"undici:request:create\", // When a new request is created\n \"undici:request:bodySent\", // When the request body is sent\n \"undici:request:headers\", // When request headers are sent\n \"undici:request:error\", // When a request encounters an error\n \"undici:request:trailers\", // When a response completes.\n\n \"undici:client:sendHeaders\",\n \"undici:client:beforeConnect\",\n \"undici:client:connected\",\n \"undici:client:connectError\",\n\n \"undici:socket:close\", // When a socket is closed\n \"undici:socket:connect\", // When a socket connects\n \"undici:socket:error\", // When a socket encounters an error\n\n \"undici:pool:request\", // When a request is added to the pool\n \"undici:pool:connect\", // When a pool creates a new connection\n \"undici:pool:disconnect\", // When a pool connection is closed\n \"undici:pool:destroy\", // When a pool is destroyed\n \"undici:dispatcher:request\", // When a dispatcher processes a request\n \"undici:dispatcher:connect\", // When a dispatcher connects\n \"undici:dispatcher:disconnect\", // When a dispatcher disconnects\n \"undici:dispatcher:retry\", // When a dispatcher retries a request\n];\n"],"mappings":"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"}
1
+ {"version":3,"file":"network_check.js","names":[],"sources":["../../src/network_check/network_check.ts"],"sourcesContent":["/** A utility to check network problems and gather statistics.\n * It's useful when we cannot connect to the server of a company\n * because of security reasons,\n * but they can send us and their DevOps team this report.\n *\n * What we check:\n * - pings to backend\n * - block registry for block overview and ui.\n * - autoupdate CDN.\n * - upload workflow to backend (workflow part via our API).\n * - the desktop could do multipart upload.\n * - the desktop could download files from S3.\n * - backend could download software and run it.\n * - backend could run python software.\n * - try to get something from every storage to work storage.\n *\n * We don't check backend access to S3 storage, it is checked on the start of backend.\n */\n\nimport type { AuthInformation, PlClientConfig } from \"@milaboratories/pl-client\";\nimport { PlClient, UnauthenticatedPlClient, plAddressToConfig } from \"@milaboratories/pl-client\";\nimport type { MiLogger, Signer } from \"@milaboratories/ts-helpers\";\nimport { ConsoleLoggerAdapter, HmacSha256Signer } from \"@milaboratories/ts-helpers\";\nimport { channel } from \"node:diagnostics_channel\";\nimport type { ClientDownload, ClientUpload } from \"@milaboratories/pl-drivers\";\nimport { LsDriver, createDownloadClient, createUploadBlobClient } from \"@milaboratories/pl-drivers\";\nimport type { HttpNetworkReport, NetworkReport } from \"./pings\";\nimport {\n autoUpdateCdnPings,\n backendPings,\n blockGARegistryOverviewPings,\n blockGARegistryUiPings,\n blockRegistryOverviewPings,\n blockRegistryUiPings,\n reportToString,\n} from \"./pings\";\nimport type { Dispatcher } from \"undici\";\nimport type { TemplateReport } from \"./template\";\nimport {\n uploadTemplate,\n uploadFile,\n downloadFile,\n createTempFile,\n pythonSoftware,\n softwareCheck,\n createBigTempFile,\n downloadFromEveryStorage,\n} from \"./template\";\nimport { randomUUID } from \"node:crypto\";\n\n/** All reports we need to collect. */\ninterface NetworkReports {\n plPings: NetworkReport<string>[];\n\n blockRegistryOverviewChecks: HttpNetworkReport[];\n blockGARegistryOverviewChecks: HttpNetworkReport[];\n blockRegistryUiChecks: HttpNetworkReport[];\n blockGARegistryUiChecks: HttpNetworkReport[];\n\n autoUpdateCdnChecks: HttpNetworkReport[];\n\n uploadTemplateCheck: TemplateReport;\n uploadFileCheck: TemplateReport;\n downloadFileCheck: TemplateReport;\n softwareCheck: TemplateReport;\n pythonSoftwareCheck: TemplateReport;\n storageToDownloadReport: Record<string, TemplateReport>;\n}\n\nexport interface CheckNetworkOpts {\n /** Signal to abort all network checks. */\n signal?: AbortSignal;\n\n /** Platforma Backend pings options. */\n pingCheckDurationMs: number;\n pingTimeoutMs: number;\n maxPingsPerSecond: number;\n\n /** An options for CDN and block registry. */\n httpTimeoutMs: number;\n\n /** Block registry pings options. */\n blockRegistryDurationMs: number;\n maxRegistryChecksPerSecond: number;\n blockRegistryUrl: string;\n blockGARegistryUrl: string;\n blockOverviewPath: string;\n blockUiPath: string;\n\n /** CDN for auto-update pings options. */\n autoUpdateCdnDurationMs: number;\n maxAutoUpdateCdnChecksPerSecond: number;\n autoUpdateCdnUrl: string;\n\n /** Body limit for requests. */\n bodyLimit: number;\n\n /** Limit for the size of files to download from every storage. */\n everyStorageBytesLimit: number;\n /** Minimal size of files to create a directory from for every storage. */\n everyStorageMinFileSize: number;\n /** Maximal size of files to create a directory from for every storage. */\n everyStorageMaxFileSize: number;\n /** How many files to check from every storage. */\n everyStorageNFilesToCheck: number;\n /** Minimal number of ls requests for every storage. */\n everyStorageMinLsRequests: number;\n}\n\n/** Checks connectivity to Platforma Backend, to block registry\n * and to auto-update CDN,\n * and generates a string report. */\nexport async function checkNetwork(\n plCredentials: string,\n plUser: string | undefined,\n plPassword: string | undefined,\n optsOverrides: Partial<CheckNetworkOpts> = {},\n): Promise<string> {\n const undiciLogs: any[] = [];\n // Subscribe to all Undici diagnostic events\n undiciEvents.forEach((event) => {\n const diagnosticChannel = channel(event);\n diagnosticChannel.subscribe((message: any) => {\n const timestamp = new Date().toISOString();\n const data = { ...message };\n if (data?.response?.headers) {\n data.response = { ...data.response };\n data.response.headers = data.response.headers.slice();\n data.response.headers = data.response.headers.map((h: any) => h.toString());\n }\n\n // we try to upload big files, don't include the buffer in the report.\n if (data?.request?.body) {\n data.request = { ...data.request };\n data.request.body = `too big`;\n }\n\n undiciLogs.push(\n JSON.stringify({\n timestamp,\n event,\n data,\n }),\n );\n });\n });\n\n try {\n const {\n logger,\n plConfig,\n client,\n signer,\n downloadClient,\n uploadBlobClient,\n lsDriver,\n httpClient,\n ops,\n } = await initNetworkCheck(plCredentials, plUser, plPassword, optsOverrides);\n\n const { filePath: filePathToDownload, fileContent: fileContentToDownload } =\n await createTempFile();\n const { filePath: filePathToUpload } = await createBigTempFile();\n\n const report: NetworkReports = {\n plPings: await backendPings(ops, plConfig),\n blockRegistryOverviewChecks: await blockRegistryOverviewPings(ops, httpClient),\n blockGARegistryOverviewChecks: await blockGARegistryOverviewPings(ops, httpClient),\n blockRegistryUiChecks: await blockRegistryUiPings(ops, httpClient),\n blockGARegistryUiChecks: await blockGARegistryUiPings(ops, httpClient),\n\n autoUpdateCdnChecks: await autoUpdateCdnPings(ops, httpClient),\n\n uploadTemplateCheck: await uploadTemplate(logger, client, \"Jack\"),\n uploadFileCheck: await uploadFile(\n logger,\n signer,\n lsDriver,\n uploadBlobClient,\n client,\n filePathToUpload,\n ),\n downloadFileCheck: await downloadFile(\n logger,\n client,\n lsDriver,\n uploadBlobClient,\n downloadClient,\n filePathToDownload,\n fileContentToDownload,\n ),\n softwareCheck: await softwareCheck(client),\n pythonSoftwareCheck: await pythonSoftware(client, \"Jack\"),\n storageToDownloadReport: await downloadFromEveryStorage(logger, client, lsDriver, {\n minLsRequests: ops.everyStorageMinLsRequests,\n bytesLimit: ops.everyStorageBytesLimit,\n minFileSize: ops.everyStorageMinFileSize,\n maxFileSize: ops.everyStorageMaxFileSize,\n nFilesToCheck: ops.everyStorageNFilesToCheck,\n }),\n };\n\n return reportsToString(report, plCredentials, ops, undiciLogs);\n } catch (e) {\n return `Unhandled error while checking the network: ${e}`;\n }\n}\n\nexport async function initNetworkCheck(\n plCredentials: string,\n plUser: string | undefined,\n plPassword: string | undefined,\n optsOverrides: Partial<CheckNetworkOpts> = {},\n): Promise<{\n logger: MiLogger;\n plConfig: PlClientConfig;\n signer: Signer;\n client: PlClient;\n downloadClient: ClientDownload;\n uploadBlobClient: ClientUpload;\n lsDriver: LsDriver;\n httpClient: Dispatcher;\n ops: CheckNetworkOpts;\n terminate: () => Promise<void>;\n}> {\n const ops: CheckNetworkOpts = {\n pingCheckDurationMs: 10000,\n pingTimeoutMs: 3000,\n maxPingsPerSecond: 50,\n\n httpTimeoutMs: 3000,\n\n blockRegistryDurationMs: 3000,\n maxRegistryChecksPerSecond: 1,\n\n blockRegistryUrl: \"https://blocks.pl-open.science\",\n blockGARegistryUrl: \"https://blocks-ga.pl-open.science\",\n blockOverviewPath: \"v2/overview.json\",\n blockUiPath: \"v2/milaboratories/samples-and-data/1.7.0/ui.tgz\",\n\n autoUpdateCdnDurationMs: 5000,\n maxAutoUpdateCdnChecksPerSecond: 1,\n autoUpdateCdnUrl:\n \"https://cdn.platforma.bio/software/platforma-desktop-v2/windows/amd64/latest.yml\",\n\n bodyLimit: 300,\n\n everyStorageBytesLimit: 1024,\n everyStorageMinFileSize: 1024,\n everyStorageMaxFileSize: 200 * 1024 * 1024, // 200 MB\n everyStorageNFilesToCheck: 300,\n everyStorageMinLsRequests: 50,\n ...optsOverrides,\n };\n\n const plConfig = plAddressToConfig(plCredentials, {\n defaultRequestTimeout: ops.pingTimeoutMs,\n });\n\n // exposing alternative root for fields not to interfere with\n // projects of the user.\n plConfig.alternativeRoot = `check_network_${randomUUID()}`;\n\n const uaClient = await UnauthenticatedPlClient.build(plConfig);\n\n let auth: AuthInformation = {};\n if (plUser && plPassword) {\n auth = await uaClient.login(plUser, plPassword);\n }\n\n const client = await PlClient.init(plCredentials, { authInformation: auth });\n\n const httpClient = uaClient.ll.httpDispatcher;\n const logger = new ConsoleLoggerAdapter();\n\n // FIXME: do we need to get an actual secret?\n const signer = new HmacSha256Signer(\"localSecret\");\n\n // We could initialize middle-layer here, but for now it seems like an overkill.\n // Here's the code to do it:\n //\n // const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'platforma-network-check-'));\n // const ml = await MiddleLayer.init(client, tmpDir, {\n // logger,\n // localSecret: '',\n // localProjections: [],\n // openFileDialogCallback: () => Promise.resolve([]),\n // preferredUpdateChannel: 'stable',\n // });\n\n const downloadClient = createDownloadClient(logger, client, []);\n const uploadBlobClient = createUploadBlobClient(client, logger);\n\n const lsDriver = await LsDriver.init(logger, client, signer, [], () => Promise.resolve([]), []);\n\n const terminate = async () => {\n downloadClient.close();\n uploadBlobClient.close();\n await httpClient.close();\n await client.close();\n };\n\n return {\n logger,\n plConfig,\n client,\n signer,\n downloadClient,\n uploadBlobClient,\n lsDriver,\n httpClient,\n ops,\n terminate,\n };\n}\n\nfunction reportsToString(\n report: NetworkReports,\n plEndpoint: string,\n opts: CheckNetworkOpts,\n undiciLogs: any[],\n): string {\n const successPings = report.plPings.filter((p) => p.response.ok);\n const failedPings = report.plPings.filter((p) => !p.response.ok);\n const successPingsBodies = [\n ...new Set(successPings.map((p) => JSON.stringify((p.response as any).value))),\n ];\n\n const summary = (ok: boolean) => (ok ? \"OK\" : \"FAILED\");\n const templateSummary = (report: TemplateReport) =>\n report.status === \"ok\" ? \"OK\" : report.status === \"warn\" ? \"WARN\" : \"FAILED\";\n\n const pings = reportToString(report.plPings);\n const blockRegistryOverview = reportToString(report.blockRegistryOverviewChecks);\n const blockGARegistryOverview = reportToString(report.blockGARegistryOverviewChecks);\n const blockRegistryUi = reportToString(report.blockRegistryUiChecks);\n const blockGARegistryUi = reportToString(report.blockGARegistryUiChecks);\n const autoUpdateCdn = reportToString(report.autoUpdateCdnChecks);\n\n const storagesSummary = Object.entries(report.storageToDownloadReport)\n .map(([storage, report]) => `${templateSummary(report)} ${storage} storage check`)\n .join(\"\\n\");\n\n return `\n${summary(pings.ok)} pings to Platforma Backend\n${summary(blockRegistryOverview.ok)} block registry overview\n${summary(blockGARegistryOverview.ok)} block ga registry overview\n${summary(blockRegistryUi.ok)} block registry ui\n${summary(blockGARegistryUi.ok)} block ga registry ui\n${summary(autoUpdateCdn.ok)} auto-update CDN\n${templateSummary(report.uploadTemplateCheck)} upload template\n${templateSummary(report.uploadFileCheck)} upload file\n${templateSummary(report.downloadFileCheck)} download file\n${templateSummary(report.softwareCheck)} software check\n${templateSummary(report.pythonSoftwareCheck)} python software check\n${storagesSummary}\n\ndetails:\npl endpoint: ${plEndpoint};\noptions: ${JSON.stringify(opts, null, 2)}.\n\nUpload template response: ${report.uploadTemplateCheck.message}\n\nUpload file response: ${report.uploadFileCheck.message}\n\nDownload file response: ${report.downloadFileCheck.message}\n\nSoftware check response: ${report.softwareCheck.message}\nPython software check response: ${report.pythonSoftwareCheck.message}\nStorage to download responses: ${JSON.stringify(report.storageToDownloadReport, null, 2)}\n\nPlatforma pings: ${pings.details}\n\nBlock registry overview responses: ${blockRegistryOverview.details}\n\nBlock ga registry overview responses: ${blockGARegistryOverview.details}\n\nBlock registry ui responses: ${blockRegistryUi.details}\n\nBlock ga registry ui responses: ${blockGARegistryUi.details}\n\nAuto-update CDN responses: ${autoUpdateCdn.details}\n\ndumps:\nBlock registry overview dumps:\n${JSON.stringify(report.blockRegistryOverviewChecks, null, 2)}\n\nBlock ga registry overview dumps:\n${JSON.stringify(report.blockGARegistryOverviewChecks, null, 2)}\n\nBlock registry ui dumps:\n${JSON.stringify(report.blockRegistryUiChecks, null, 2)}\n\nBlock ga registry ui dumps:\n${JSON.stringify(report.blockGARegistryUiChecks, null, 2)}\n\nAuto-update CDN dumps:\n${JSON.stringify(report.autoUpdateCdnChecks, null, 2)}\n\nPlatforma pings error dumps:\n${JSON.stringify(failedPings, null, 2)}\n\nPlatforma pings success dump examples:\n${JSON.stringify(successPingsBodies, null, 2)}\n\nUndici logs:\n${undiciLogs.join(\"\\n\")}\n`;\n}\n\n// List of Undici diagnostic channels\nconst undiciEvents: string[] = [\n \"undici:request:create\", // When a new request is created\n \"undici:request:bodySent\", // When the request body is sent\n \"undici:request:headers\", // When request headers are sent\n \"undici:request:error\", // When a request encounters an error\n \"undici:request:trailers\", // When a response completes.\n\n \"undici:client:sendHeaders\",\n \"undici:client:beforeConnect\",\n \"undici:client:connected\",\n \"undici:client:connectError\",\n\n \"undici:socket:close\", // When a socket is closed\n \"undici:socket:connect\", // When a socket connects\n \"undici:socket:error\", // When a socket encounters an error\n\n \"undici:pool:request\", // When a request is added to the pool\n \"undici:pool:connect\", // When a pool creates a new connection\n \"undici:pool:disconnect\", // When a pool connection is closed\n \"undici:pool:destroy\", // When a pool is destroyed\n \"undici:dispatcher:request\", // When a dispatcher processes a request\n \"undici:dispatcher:connect\", // When a dispatcher connects\n \"undici:dispatcher:disconnect\", // When a dispatcher disconnects\n \"undici:dispatcher:retry\", // When a dispatcher retries a request\n];\n"],"mappings":"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"}
@@ -8,29 +8,29 @@ async function backendPings(ops, plConfig) {
8
8
  return await recordPings(ops.pingCheckDurationMs, ops.maxPingsPerSecond, async () => {
9
9
  const response = await (await _milaboratories_pl_client.UnauthenticatedPlClient.build(plConfig)).ping();
10
10
  return JSON.stringify(response).slice(0, ops.bodyLimit) + "...";
11
- });
11
+ }, ops.signal);
12
12
  }
13
13
  async function blockRegistryOverviewPings(ops, httpClient) {
14
- return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockOverviewPath, ops.blockRegistryUrl), ops, httpClient));
14
+ return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockOverviewPath, ops.blockRegistryUrl), ops, httpClient), ops.signal);
15
15
  }
16
16
  async function blockGARegistryOverviewPings(ops, httpClient) {
17
- return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockOverviewPath, ops.blockGARegistryUrl), ops, httpClient));
17
+ return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockOverviewPath, ops.blockGARegistryUrl), ops, httpClient), ops.signal);
18
18
  }
19
19
  async function blockRegistryUiPings(ops, httpClient) {
20
- return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockUiPath, ops.blockRegistryUrl), ops, httpClient));
20
+ return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockUiPath, ops.blockRegistryUrl), ops, httpClient), ops.signal);
21
21
  }
22
22
  async function blockGARegistryUiPings(ops, httpClient) {
23
- return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockUiPath, ops.blockGARegistryUrl), ops, httpClient));
23
+ return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockUiPath, ops.blockGARegistryUrl), ops, httpClient), ops.signal);
24
24
  }
25
25
  async function autoUpdateCdnPings(ops, httpClient) {
26
- return await recordPings(ops.autoUpdateCdnDurationMs, ops.maxAutoUpdateCdnChecksPerSecond, async () => await requestUrl(ops.autoUpdateCdnUrl, ops, httpClient));
26
+ return await recordPings(ops.autoUpdateCdnDurationMs, ops.maxAutoUpdateCdnChecksPerSecond, async () => await requestUrl(ops.autoUpdateCdnUrl, ops, httpClient), ops.signal);
27
27
  }
28
28
  /** Executes a body several times per second up to the given duration,
29
29
  * and returns results and elapsed time for every result. */
30
- async function recordPings(pingCheckDurationMs, maxPingsPerSecond, body) {
30
+ async function recordPings(pingCheckDurationMs, maxPingsPerSecond, body, signal) {
31
31
  const startPings = Date.now();
32
32
  const reports = [];
33
- while (elapsed(startPings) < pingCheckDurationMs) {
33
+ while (elapsed(startPings) < pingCheckDurationMs && !signal?.aborted) {
34
34
  const startPing = Date.now();
35
35
  let response;
36
36
  try {
@@ -50,7 +50,12 @@ async function recordPings(pingCheckDurationMs, maxPingsPerSecond, body) {
50
50
  response
51
51
  });
52
52
  const sleepBetweenPings = 1e3 / maxPingsPerSecond - elapsedPing;
53
- if (sleepBetweenPings > 0) await (0, node_timers_promises.setTimeout)(sleepBetweenPings);
53
+ if (sleepBetweenPings > 0) try {
54
+ await (0, node_timers_promises.setTimeout)(sleepBetweenPings, void 0, signal ? { signal } : void 0);
55
+ } catch (e) {
56
+ if (e instanceof Error && e.name === "AbortError") break;
57
+ throw e;
58
+ }
54
59
  }
55
60
  return reports;
56
61
  }
@@ -85,6 +90,10 @@ function reportToString(report) {
85
90
  };
86
91
  }
87
92
  function elapsedStat(reports) {
93
+ if (reports.length === 0) return {
94
+ mean: 0,
95
+ median: void 0
96
+ };
88
97
  const checks = reports.map((p) => p.elapsedMs).sort();
89
98
  const mean = checks.reduce((sum, p) => sum + p) / checks.length;
90
99
  let median = void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"pings.cjs","names":["UnauthenticatedPlClient"],"sources":["../../src/network_check/pings.ts"],"sourcesContent":["import type { ValueOrError } from \"@milaboratories/ts-helpers\";\nimport { setTimeout } from \"node:timers/promises\";\nimport { request } from \"undici\";\nimport type { Dispatcher } from \"undici\";\nimport type { CheckNetworkOpts } from \"./network_check\";\nimport { UnauthenticatedPlClient, type PlClientConfig } from \"@milaboratories/pl-client\";\n\n/** A report about one concrete ping to the service. */\nexport interface NetworkReport<T> {\n elapsedMs: number;\n response: ValueOrError<T>;\n}\n\nexport type HttpNetworkReport = NetworkReport<{\n statusCode: number;\n beginningOfBody: string;\n}>;\n\nexport async function backendPings(\n ops: CheckNetworkOpts,\n plConfig: PlClientConfig,\n): Promise<NetworkReport<string>[]> {\n return await recordPings(ops.pingCheckDurationMs, ops.maxPingsPerSecond, async () => {\n const uaClient = await UnauthenticatedPlClient.build(plConfig);\n const response = await uaClient.ping();\n return JSON.stringify(response).slice(0, ops.bodyLimit) + \"...\";\n });\n}\n\nexport async function blockRegistryOverviewPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () =>\n await requestUrl(new URL(ops.blockOverviewPath, ops.blockRegistryUrl), ops, httpClient),\n );\n}\n\nexport async function blockGARegistryOverviewPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () =>\n await requestUrl(new URL(ops.blockOverviewPath, ops.blockGARegistryUrl), ops, httpClient),\n );\n}\n\nexport async function blockRegistryUiPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () => await requestUrl(new URL(ops.blockUiPath, ops.blockRegistryUrl), ops, httpClient),\n );\n}\n\nexport async function blockGARegistryUiPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () => await requestUrl(new URL(ops.blockUiPath, ops.blockGARegistryUrl), ops, httpClient),\n );\n}\n\nexport async function autoUpdateCdnPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.autoUpdateCdnDurationMs,\n ops.maxAutoUpdateCdnChecksPerSecond,\n async () => await requestUrl(ops.autoUpdateCdnUrl, ops, httpClient),\n );\n}\n\n/** Executes a body several times per second up to the given duration,\n * and returns results and elapsed time for every result. */\nexport async function recordPings<T>(\n pingCheckDurationMs: number,\n maxPingsPerSecond: number,\n body: () => Promise<T>,\n): Promise<NetworkReport<T>[]> {\n const startPings = Date.now();\n const reports: NetworkReport<T>[] = [];\n\n while (elapsed(startPings) < pingCheckDurationMs) {\n const startPing = Date.now();\n let response: ValueOrError<T>;\n try {\n response = { ok: true, value: await body() };\n } catch (e) {\n response = { ok: false, error: e };\n }\n const elapsedPing = elapsed(startPing);\n\n reports.push({\n elapsedMs: elapsedPing,\n response,\n });\n\n const sleepBetweenPings = 1000 / maxPingsPerSecond - elapsedPing;\n\n if (sleepBetweenPings > 0) {\n await setTimeout(sleepBetweenPings);\n }\n }\n\n return reports;\n}\n\nexport async function requestUrl(url: string | URL, ops: CheckNetworkOpts, httpClient: Dispatcher) {\n const { body: rawBody, statusCode } = await request(url, {\n dispatcher: httpClient,\n headersTimeout: ops.httpTimeoutMs,\n bodyTimeout: ops.httpTimeoutMs,\n });\n const body = await rawBody.text();\n\n return {\n statusCode: statusCode,\n beginningOfBody: body.slice(0, ops.bodyLimit) + \"...\",\n };\n}\n\nexport function elapsed(startMs: number): number {\n return Date.now() - startMs;\n}\n\nexport function reportToString<T>(report: NetworkReport<T>[]): {\n ok: boolean;\n details: string;\n} {\n const successes = report.filter((r) => r.response.ok);\n const errorsLen = report.length - successes.length;\n const { mean, median } = elapsedStat(report);\n\n const details = `\n total: ${report.length};\n successes: ${successes.length};\n errors: ${errorsLen};\n mean in ms: ${mean};\n median in ms: ${median};\n `;\n\n return {\n ok: errorsLen === 0,\n details,\n };\n}\n\nfunction elapsedStat(reports: { elapsedMs: number }[]) {\n const checks = reports.map((p) => p.elapsedMs).sort();\n const mean = checks.reduce((sum, p) => sum + p) / checks.length;\n\n let median = undefined;\n if (checks.length > 0) {\n const mid = Math.floor(checks.length / 2);\n median = checks.length % 2 ? checks[mid] : (checks[mid - 1] + checks[mid]) / 2;\n }\n\n return { mean, median };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pings.cjs","names":["UnauthenticatedPlClient"],"sources":["../../src/network_check/pings.ts"],"sourcesContent":["import type { ValueOrError } from \"@milaboratories/ts-helpers\";\nimport { setTimeout } from \"node:timers/promises\";\nimport { request } from \"undici\";\nimport type { Dispatcher } from \"undici\";\nimport type { CheckNetworkOpts } from \"./network_check\";\nimport { UnauthenticatedPlClient, type PlClientConfig } from \"@milaboratories/pl-client\";\n\n/** A report about one concrete ping to the service. */\nexport interface NetworkReport<T> {\n elapsedMs: number;\n response: ValueOrError<T>;\n}\n\nexport type HttpNetworkReport = NetworkReport<{\n statusCode: number;\n beginningOfBody: string;\n}>;\n\nexport async function backendPings(\n ops: CheckNetworkOpts,\n plConfig: PlClientConfig,\n): Promise<NetworkReport<string>[]> {\n return await recordPings(\n ops.pingCheckDurationMs,\n ops.maxPingsPerSecond,\n async () => {\n const uaClient = await UnauthenticatedPlClient.build(plConfig);\n const response = await uaClient.ping();\n return JSON.stringify(response).slice(0, ops.bodyLimit) + \"...\";\n },\n ops.signal,\n );\n}\n\nexport async function blockRegistryOverviewPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () =>\n await requestUrl(new URL(ops.blockOverviewPath, ops.blockRegistryUrl), ops, httpClient),\n ops.signal,\n );\n}\n\nexport async function blockGARegistryOverviewPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () =>\n await requestUrl(new URL(ops.blockOverviewPath, ops.blockGARegistryUrl), ops, httpClient),\n ops.signal,\n );\n}\n\nexport async function blockRegistryUiPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () => await requestUrl(new URL(ops.blockUiPath, ops.blockRegistryUrl), ops, httpClient),\n ops.signal,\n );\n}\n\nexport async function blockGARegistryUiPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () => await requestUrl(new URL(ops.blockUiPath, ops.blockGARegistryUrl), ops, httpClient),\n ops.signal,\n );\n}\n\nexport async function autoUpdateCdnPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.autoUpdateCdnDurationMs,\n ops.maxAutoUpdateCdnChecksPerSecond,\n async () => await requestUrl(ops.autoUpdateCdnUrl, ops, httpClient),\n ops.signal,\n );\n}\n\n/** Executes a body several times per second up to the given duration,\n * and returns results and elapsed time for every result. */\nexport async function recordPings<T>(\n pingCheckDurationMs: number,\n maxPingsPerSecond: number,\n body: () => Promise<T>,\n signal?: AbortSignal,\n): Promise<NetworkReport<T>[]> {\n const startPings = Date.now();\n const reports: NetworkReport<T>[] = [];\n\n while (elapsed(startPings) < pingCheckDurationMs && !signal?.aborted) {\n const startPing = Date.now();\n let response: ValueOrError<T>;\n try {\n response = { ok: true, value: await body() };\n } catch (e) {\n response = { ok: false, error: e };\n }\n const elapsedPing = elapsed(startPing);\n\n reports.push({\n elapsedMs: elapsedPing,\n response,\n });\n\n const sleepBetweenPings = 1000 / maxPingsPerSecond - elapsedPing;\n\n if (sleepBetweenPings > 0) {\n try {\n await setTimeout(sleepBetweenPings, undefined, signal ? { signal } : undefined);\n } catch (e: unknown) {\n if (e instanceof Error && e.name === \"AbortError\") break;\n throw e;\n }\n }\n }\n\n return reports;\n}\n\nexport async function requestUrl(url: string | URL, ops: CheckNetworkOpts, httpClient: Dispatcher) {\n const { body: rawBody, statusCode } = await request(url, {\n dispatcher: httpClient,\n headersTimeout: ops.httpTimeoutMs,\n bodyTimeout: ops.httpTimeoutMs,\n });\n const body = await rawBody.text();\n\n return {\n statusCode: statusCode,\n beginningOfBody: body.slice(0, ops.bodyLimit) + \"...\",\n };\n}\n\nexport function elapsed(startMs: number): number {\n return Date.now() - startMs;\n}\n\nexport function reportToString<T>(report: NetworkReport<T>[]): {\n ok: boolean;\n details: string;\n} {\n const successes = report.filter((r) => r.response.ok);\n const errorsLen = report.length - successes.length;\n const { mean, median } = elapsedStat(report);\n\n const details = `\n total: ${report.length};\n successes: ${successes.length};\n errors: ${errorsLen};\n mean in ms: ${mean};\n median in ms: ${median};\n `;\n\n return {\n ok: errorsLen === 0,\n details,\n };\n}\n\nfunction elapsedStat(reports: { elapsedMs: number }[]) {\n if (reports.length === 0) return { mean: 0, median: undefined };\n const checks = reports.map((p) => p.elapsedMs).sort();\n const mean = checks.reduce((sum, p) => sum + p) / checks.length;\n\n let median = undefined;\n if (checks.length > 0) {\n const mid = Math.floor(checks.length / 2);\n median = checks.length % 2 ? checks[mid] : (checks[mid - 1] + checks[mid]) / 2;\n }\n\n return { mean, median };\n}\n"],"mappings":"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"}
@@ -7,29 +7,29 @@ async function backendPings(ops, plConfig) {
7
7
  return await recordPings(ops.pingCheckDurationMs, ops.maxPingsPerSecond, async () => {
8
8
  const response = await (await UnauthenticatedPlClient.build(plConfig)).ping();
9
9
  return JSON.stringify(response).slice(0, ops.bodyLimit) + "...";
10
- });
10
+ }, ops.signal);
11
11
  }
12
12
  async function blockRegistryOverviewPings(ops, httpClient) {
13
- return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockOverviewPath, ops.blockRegistryUrl), ops, httpClient));
13
+ return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockOverviewPath, ops.blockRegistryUrl), ops, httpClient), ops.signal);
14
14
  }
15
15
  async function blockGARegistryOverviewPings(ops, httpClient) {
16
- return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockOverviewPath, ops.blockGARegistryUrl), ops, httpClient));
16
+ return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockOverviewPath, ops.blockGARegistryUrl), ops, httpClient), ops.signal);
17
17
  }
18
18
  async function blockRegistryUiPings(ops, httpClient) {
19
- return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockUiPath, ops.blockRegistryUrl), ops, httpClient));
19
+ return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockUiPath, ops.blockRegistryUrl), ops, httpClient), ops.signal);
20
20
  }
21
21
  async function blockGARegistryUiPings(ops, httpClient) {
22
- return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockUiPath, ops.blockGARegistryUrl), ops, httpClient));
22
+ return await recordPings(ops.blockRegistryDurationMs, ops.maxRegistryChecksPerSecond, async () => await requestUrl(new URL(ops.blockUiPath, ops.blockGARegistryUrl), ops, httpClient), ops.signal);
23
23
  }
24
24
  async function autoUpdateCdnPings(ops, httpClient) {
25
- return await recordPings(ops.autoUpdateCdnDurationMs, ops.maxAutoUpdateCdnChecksPerSecond, async () => await requestUrl(ops.autoUpdateCdnUrl, ops, httpClient));
25
+ return await recordPings(ops.autoUpdateCdnDurationMs, ops.maxAutoUpdateCdnChecksPerSecond, async () => await requestUrl(ops.autoUpdateCdnUrl, ops, httpClient), ops.signal);
26
26
  }
27
27
  /** Executes a body several times per second up to the given duration,
28
28
  * and returns results and elapsed time for every result. */
29
- async function recordPings(pingCheckDurationMs, maxPingsPerSecond, body) {
29
+ async function recordPings(pingCheckDurationMs, maxPingsPerSecond, body, signal) {
30
30
  const startPings = Date.now();
31
31
  const reports = [];
32
- while (elapsed(startPings) < pingCheckDurationMs) {
32
+ while (elapsed(startPings) < pingCheckDurationMs && !signal?.aborted) {
33
33
  const startPing = Date.now();
34
34
  let response;
35
35
  try {
@@ -49,7 +49,12 @@ async function recordPings(pingCheckDurationMs, maxPingsPerSecond, body) {
49
49
  response
50
50
  });
51
51
  const sleepBetweenPings = 1e3 / maxPingsPerSecond - elapsedPing;
52
- if (sleepBetweenPings > 0) await setTimeout(sleepBetweenPings);
52
+ if (sleepBetweenPings > 0) try {
53
+ await setTimeout(sleepBetweenPings, void 0, signal ? { signal } : void 0);
54
+ } catch (e) {
55
+ if (e instanceof Error && e.name === "AbortError") break;
56
+ throw e;
57
+ }
53
58
  }
54
59
  return reports;
55
60
  }
@@ -84,6 +89,10 @@ function reportToString(report) {
84
89
  };
85
90
  }
86
91
  function elapsedStat(reports) {
92
+ if (reports.length === 0) return {
93
+ mean: 0,
94
+ median: void 0
95
+ };
87
96
  const checks = reports.map((p) => p.elapsedMs).sort();
88
97
  const mean = checks.reduce((sum, p) => sum + p) / checks.length;
89
98
  let median = void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"pings.js","names":[],"sources":["../../src/network_check/pings.ts"],"sourcesContent":["import type { ValueOrError } from \"@milaboratories/ts-helpers\";\nimport { setTimeout } from \"node:timers/promises\";\nimport { request } from \"undici\";\nimport type { Dispatcher } from \"undici\";\nimport type { CheckNetworkOpts } from \"./network_check\";\nimport { UnauthenticatedPlClient, type PlClientConfig } from \"@milaboratories/pl-client\";\n\n/** A report about one concrete ping to the service. */\nexport interface NetworkReport<T> {\n elapsedMs: number;\n response: ValueOrError<T>;\n}\n\nexport type HttpNetworkReport = NetworkReport<{\n statusCode: number;\n beginningOfBody: string;\n}>;\n\nexport async function backendPings(\n ops: CheckNetworkOpts,\n plConfig: PlClientConfig,\n): Promise<NetworkReport<string>[]> {\n return await recordPings(ops.pingCheckDurationMs, ops.maxPingsPerSecond, async () => {\n const uaClient = await UnauthenticatedPlClient.build(plConfig);\n const response = await uaClient.ping();\n return JSON.stringify(response).slice(0, ops.bodyLimit) + \"...\";\n });\n}\n\nexport async function blockRegistryOverviewPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () =>\n await requestUrl(new URL(ops.blockOverviewPath, ops.blockRegistryUrl), ops, httpClient),\n );\n}\n\nexport async function blockGARegistryOverviewPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () =>\n await requestUrl(new URL(ops.blockOverviewPath, ops.blockGARegistryUrl), ops, httpClient),\n );\n}\n\nexport async function blockRegistryUiPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () => await requestUrl(new URL(ops.blockUiPath, ops.blockRegistryUrl), ops, httpClient),\n );\n}\n\nexport async function blockGARegistryUiPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () => await requestUrl(new URL(ops.blockUiPath, ops.blockGARegistryUrl), ops, httpClient),\n );\n}\n\nexport async function autoUpdateCdnPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.autoUpdateCdnDurationMs,\n ops.maxAutoUpdateCdnChecksPerSecond,\n async () => await requestUrl(ops.autoUpdateCdnUrl, ops, httpClient),\n );\n}\n\n/** Executes a body several times per second up to the given duration,\n * and returns results and elapsed time for every result. */\nexport async function recordPings<T>(\n pingCheckDurationMs: number,\n maxPingsPerSecond: number,\n body: () => Promise<T>,\n): Promise<NetworkReport<T>[]> {\n const startPings = Date.now();\n const reports: NetworkReport<T>[] = [];\n\n while (elapsed(startPings) < pingCheckDurationMs) {\n const startPing = Date.now();\n let response: ValueOrError<T>;\n try {\n response = { ok: true, value: await body() };\n } catch (e) {\n response = { ok: false, error: e };\n }\n const elapsedPing = elapsed(startPing);\n\n reports.push({\n elapsedMs: elapsedPing,\n response,\n });\n\n const sleepBetweenPings = 1000 / maxPingsPerSecond - elapsedPing;\n\n if (sleepBetweenPings > 0) {\n await setTimeout(sleepBetweenPings);\n }\n }\n\n return reports;\n}\n\nexport async function requestUrl(url: string | URL, ops: CheckNetworkOpts, httpClient: Dispatcher) {\n const { body: rawBody, statusCode } = await request(url, {\n dispatcher: httpClient,\n headersTimeout: ops.httpTimeoutMs,\n bodyTimeout: ops.httpTimeoutMs,\n });\n const body = await rawBody.text();\n\n return {\n statusCode: statusCode,\n beginningOfBody: body.slice(0, ops.bodyLimit) + \"...\",\n };\n}\n\nexport function elapsed(startMs: number): number {\n return Date.now() - startMs;\n}\n\nexport function reportToString<T>(report: NetworkReport<T>[]): {\n ok: boolean;\n details: string;\n} {\n const successes = report.filter((r) => r.response.ok);\n const errorsLen = report.length - successes.length;\n const { mean, median } = elapsedStat(report);\n\n const details = `\n total: ${report.length};\n successes: ${successes.length};\n errors: ${errorsLen};\n mean in ms: ${mean};\n median in ms: ${median};\n `;\n\n return {\n ok: errorsLen === 0,\n details,\n };\n}\n\nfunction elapsedStat(reports: { elapsedMs: number }[]) {\n const checks = reports.map((p) => p.elapsedMs).sort();\n const mean = checks.reduce((sum, p) => sum + p) / checks.length;\n\n let median = undefined;\n if (checks.length > 0) {\n const mid = Math.floor(checks.length / 2);\n median = checks.length % 2 ? checks[mid] : (checks[mid - 1] + checks[mid]) / 2;\n }\n\n return { mean, median };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pings.js","names":[],"sources":["../../src/network_check/pings.ts"],"sourcesContent":["import type { ValueOrError } from \"@milaboratories/ts-helpers\";\nimport { setTimeout } from \"node:timers/promises\";\nimport { request } from \"undici\";\nimport type { Dispatcher } from \"undici\";\nimport type { CheckNetworkOpts } from \"./network_check\";\nimport { UnauthenticatedPlClient, type PlClientConfig } from \"@milaboratories/pl-client\";\n\n/** A report about one concrete ping to the service. */\nexport interface NetworkReport<T> {\n elapsedMs: number;\n response: ValueOrError<T>;\n}\n\nexport type HttpNetworkReport = NetworkReport<{\n statusCode: number;\n beginningOfBody: string;\n}>;\n\nexport async function backendPings(\n ops: CheckNetworkOpts,\n plConfig: PlClientConfig,\n): Promise<NetworkReport<string>[]> {\n return await recordPings(\n ops.pingCheckDurationMs,\n ops.maxPingsPerSecond,\n async () => {\n const uaClient = await UnauthenticatedPlClient.build(plConfig);\n const response = await uaClient.ping();\n return JSON.stringify(response).slice(0, ops.bodyLimit) + \"...\";\n },\n ops.signal,\n );\n}\n\nexport async function blockRegistryOverviewPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () =>\n await requestUrl(new URL(ops.blockOverviewPath, ops.blockRegistryUrl), ops, httpClient),\n ops.signal,\n );\n}\n\nexport async function blockGARegistryOverviewPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () =>\n await requestUrl(new URL(ops.blockOverviewPath, ops.blockGARegistryUrl), ops, httpClient),\n ops.signal,\n );\n}\n\nexport async function blockRegistryUiPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () => await requestUrl(new URL(ops.blockUiPath, ops.blockRegistryUrl), ops, httpClient),\n ops.signal,\n );\n}\n\nexport async function blockGARegistryUiPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.blockRegistryDurationMs,\n ops.maxRegistryChecksPerSecond,\n async () => await requestUrl(new URL(ops.blockUiPath, ops.blockGARegistryUrl), ops, httpClient),\n ops.signal,\n );\n}\n\nexport async function autoUpdateCdnPings(\n ops: CheckNetworkOpts,\n httpClient: Dispatcher,\n): Promise<HttpNetworkReport[]> {\n return await recordPings(\n ops.autoUpdateCdnDurationMs,\n ops.maxAutoUpdateCdnChecksPerSecond,\n async () => await requestUrl(ops.autoUpdateCdnUrl, ops, httpClient),\n ops.signal,\n );\n}\n\n/** Executes a body several times per second up to the given duration,\n * and returns results and elapsed time for every result. */\nexport async function recordPings<T>(\n pingCheckDurationMs: number,\n maxPingsPerSecond: number,\n body: () => Promise<T>,\n signal?: AbortSignal,\n): Promise<NetworkReport<T>[]> {\n const startPings = Date.now();\n const reports: NetworkReport<T>[] = [];\n\n while (elapsed(startPings) < pingCheckDurationMs && !signal?.aborted) {\n const startPing = Date.now();\n let response: ValueOrError<T>;\n try {\n response = { ok: true, value: await body() };\n } catch (e) {\n response = { ok: false, error: e };\n }\n const elapsedPing = elapsed(startPing);\n\n reports.push({\n elapsedMs: elapsedPing,\n response,\n });\n\n const sleepBetweenPings = 1000 / maxPingsPerSecond - elapsedPing;\n\n if (sleepBetweenPings > 0) {\n try {\n await setTimeout(sleepBetweenPings, undefined, signal ? { signal } : undefined);\n } catch (e: unknown) {\n if (e instanceof Error && e.name === \"AbortError\") break;\n throw e;\n }\n }\n }\n\n return reports;\n}\n\nexport async function requestUrl(url: string | URL, ops: CheckNetworkOpts, httpClient: Dispatcher) {\n const { body: rawBody, statusCode } = await request(url, {\n dispatcher: httpClient,\n headersTimeout: ops.httpTimeoutMs,\n bodyTimeout: ops.httpTimeoutMs,\n });\n const body = await rawBody.text();\n\n return {\n statusCode: statusCode,\n beginningOfBody: body.slice(0, ops.bodyLimit) + \"...\",\n };\n}\n\nexport function elapsed(startMs: number): number {\n return Date.now() - startMs;\n}\n\nexport function reportToString<T>(report: NetworkReport<T>[]): {\n ok: boolean;\n details: string;\n} {\n const successes = report.filter((r) => r.response.ok);\n const errorsLen = report.length - successes.length;\n const { mean, median } = elapsedStat(report);\n\n const details = `\n total: ${report.length};\n successes: ${successes.length};\n errors: ${errorsLen};\n mean in ms: ${mean};\n median in ms: ${median};\n `;\n\n return {\n ok: errorsLen === 0,\n details,\n };\n}\n\nfunction elapsedStat(reports: { elapsedMs: number }[]) {\n if (reports.length === 0) return { mean: 0, median: undefined };\n const checks = reports.map((p) => p.elapsedMs).sort();\n const mean = checks.reduce((sum, p) => sum + p) / checks.length;\n\n let median = undefined;\n if (checks.length > 0) {\n const mid = Math.floor(checks.length / 2);\n median = checks.length % 2 ? checks[mid] : (checks[mid - 1] + checks[mid]) / 2;\n }\n\n return { mean, median };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-middle-layer",
3
- "version": "1.48.16",
3
+ "version": "1.48.18",
4
4
  "description": "Pl Middle Layer",
5
5
  "keywords": [],
6
6
  "license": "UNLICENSED",
@@ -30,21 +30,21 @@
30
30
  "yaml": "^2.8.0",
31
31
  "zod": "~3.23.8",
32
32
  "@milaboratories/computable": "2.8.6",
33
- "@milaboratories/pl-client": "2.17.8",
34
33
  "@milaboratories/pl-deployments": "2.15.21",
35
- "@milaboratories/pl-drivers": "1.11.60",
36
- "@milaboratories/pl-errors": "1.1.70",
37
- "@milaboratories/pl-http": "1.2.4",
38
- "@milaboratories/pl-model-backend": "1.1.55",
39
- "@milaboratories/pl-model-common": "1.25.2",
34
+ "@milaboratories/pl-drivers": "1.11.61",
40
35
  "@milaboratories/pf-driver": "1.0.63",
41
- "@milaboratories/pl-tree": "1.8.48",
42
- "@milaboratories/ts-helpers": "1.7.3",
43
- "@milaboratories/pl-model-middle-layer": "1.12.10",
36
+ "@milaboratories/pl-client": "2.17.9",
37
+ "@milaboratories/pl-errors": "1.1.71",
38
+ "@milaboratories/pl-model-common": "1.25.2",
39
+ "@milaboratories/pl-model-backend": "1.1.56",
40
+ "@milaboratories/pl-http": "1.2.4",
44
41
  "@milaboratories/resolve-helper": "1.1.3",
42
+ "@milaboratories/pl-model-middle-layer": "1.12.10",
43
+ "@milaboratories/ts-helpers": "1.7.3",
45
44
  "@platforma-sdk/block-tools": "2.6.65",
46
- "@platforma-sdk/workflow-tengo": "5.9.1",
47
- "@platforma-sdk/model": "1.58.11"
45
+ "@milaboratories/pl-tree": "1.8.49",
46
+ "@platforma-sdk/model": "1.58.11",
47
+ "@platforma-sdk/workflow-tengo": "5.9.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "~24.5.2",
@@ -53,9 +53,9 @@
53
53
  "semver": "^7.7.2",
54
54
  "typescript": "~5.9.3",
55
55
  "vitest": "^4.0.18",
56
- "@milaboratories/build-configs": "1.5.1",
57
- "@milaboratories/ts-builder": "1.2.13",
58
- "@milaboratories/ts-configs": "1.2.2"
56
+ "@milaboratories/ts-configs": "1.2.2",
57
+ "@milaboratories/build-configs": "1.5.2",
58
+ "@milaboratories/ts-builder": "1.3.0"
59
59
  },
60
60
  "engines": {
61
61
  "node": ">=22.19.0"
@@ -308,7 +308,7 @@ export class MiddleLayer {
308
308
  }),
309
309
  runtimeCapabilities,
310
310
  quickJs,
311
- projectHelper: new ProjectHelper(quickJs),
311
+ projectHelper: new ProjectHelper(quickJs, logger),
312
312
  dispose: async () => {
313
313
  await retryHttpDispatcher.destroy();
314
314
  await driverKit.dispose();
@@ -24,7 +24,14 @@ import { getBlockParameters, blockOutputs } from "./block";
24
24
  import type { FrontendData } from "../model/frontend";
25
25
  import type { ProjectStructure } from "../model/project_model";
26
26
  import { projectFieldName } from "../model/project_model";
27
- import { cachedDeserialize, notEmpty, type MiLogger } from "@milaboratories/ts-helpers";
27
+ import {
28
+ cachedDeserialize,
29
+ notEmpty,
30
+ type MiLogger,
31
+ createInfiniteRetryState,
32
+ nextInfiniteRetryState,
33
+ type InfiniteRetryState,
34
+ } from "@milaboratories/ts-helpers";
28
35
  import type { BlockPackInfo } from "../model/block_pack";
29
36
  import type {
30
37
  ProjectOverview,
@@ -116,6 +123,7 @@ export class Project {
116
123
  }
117
124
 
118
125
  private async refreshLoop(): Promise<void> {
126
+ let retryState: InfiniteRetryState | undefined;
119
127
  while (!this.destroyed) {
120
128
  try {
121
129
  await withProject(
@@ -128,7 +136,9 @@ export class Project {
128
136
  { name: "doRefresh", lockId: this.projectLockId },
129
137
  );
130
138
  await this.activeConfigs.getValue();
131
- await setTimeout(this.env.ops.projectRefreshInterval, this.abortController.signal);
139
+ await setTimeout(this.env.ops.projectRefreshInterval, undefined, {
140
+ signal: this.abortController.signal,
141
+ });
132
142
 
133
143
  // Block computables housekeeping
134
144
  const overviewLight = await this.overviewLight.getValue();
@@ -141,25 +151,41 @@ export class Project {
141
151
  this.blockComputables.set(blockId, null);
142
152
  }
143
153
  }
154
+ retryState = undefined;
144
155
  } catch (e: unknown) {
145
156
  // If we're destroyed, exit gracefully regardless of error type
146
- if (this.destroyed) {
147
- // Log just in case, to help with debugging if something unexpected happens during shutdown
148
- this.env.logger.warn(new Error("Error during refresh loop shutdown", { cause: e }));
149
- break;
150
- }
157
+ if (this.destroyed) break;
151
158
 
152
159
  if (isNotFoundError(e)) {
153
- console.warn(
160
+ this.env.logger.warn(
154
161
  "project refresh routine terminated, because project was externally deleted",
155
162
  );
156
163
  break;
157
164
  } else if (isTimeoutOrCancelError(e)) {
158
165
  // Timeout during normal operation, continue the loop
159
166
  } else {
160
- // TODO: This stops the refresh loop permanently, leaving the project broken.
161
- // Need to decide how to handle this case.
162
- throw new Error("Unexpected exception", { cause: e });
167
+ retryState = retryState
168
+ ? nextInfiniteRetryState(retryState)
169
+ : createInfiniteRetryState({
170
+ type: "exponentialWithMaxDelayBackoff",
171
+ initialDelay: 1000,
172
+ maxDelay: 60_000,
173
+ backoffMultiplier: 2,
174
+ jitter: 0,
175
+ });
176
+ this.env.logger.error(
177
+ new Error(`[refreshLoop] unexpected exception, retrying in ${retryState.nextDelay}ms`, {
178
+ cause: e,
179
+ }),
180
+ );
181
+ try {
182
+ await setTimeout(retryState.nextDelay, undefined, {
183
+ signal: this.abortController.signal,
184
+ });
185
+ } catch {
186
+ // Aborted during retry delay, will exit via while condition or destroyed check
187
+ break;
188
+ }
163
189
  }
164
190
  }
165
191
  }