@transcend-io/cli 8.37.1 → 8.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{api-keys-Bb2BbZQe.cjs → api-keys-Bvt2HbSv.cjs} +2 -2
- package/dist/{api-keys-Bb2BbZQe.cjs.map → api-keys-Bvt2HbSv.cjs.map} +1 -1
- package/dist/{app-C9jD-f87.cjs → app-CdWFyBYu.cjs} +18 -18
- package/dist/{app-C9jD-f87.cjs.map → app-CdWFyBYu.cjs.map} +1 -1
- package/dist/bin/bash-complete.cjs +1 -1
- package/dist/bin/cli.cjs +1 -1
- package/dist/bin/deprecated-command.cjs +1 -1
- package/dist/{code-scanning-4d0zlFxk.cjs → code-scanning-BZzwKEfY.cjs} +2 -2
- package/dist/{code-scanning-4d0zlFxk.cjs.map → code-scanning-BZzwKEfY.cjs.map} +1 -1
- package/dist/{command-XJ7XPQ04.cjs → command-DNcjQs8y.cjs} +2 -2
- package/dist/{command-XJ7XPQ04.cjs.map → command-DNcjQs8y.cjs.map} +1 -1
- package/dist/{consent-manager-CCyvzvY5.cjs → consent-manager-oip5m3XC.cjs} +2 -2
- package/dist/{consent-manager-CCyvzvY5.cjs.map → consent-manager-oip5m3XC.cjs.map} +1 -1
- package/dist/{constants-wkuhlP8d.cjs → constants-K6pQQtc7.cjs} +2 -2
- package/dist/{constants-wkuhlP8d.cjs.map → constants-K6pQQtc7.cjs.map} +1 -1
- package/dist/{cron-DfEGA7Rf.cjs → cron-lijiEqFA.cjs} +2 -2
- package/dist/{cron-DfEGA7Rf.cjs.map → cron-lijiEqFA.cjs.map} +1 -1
- package/dist/{data-inventory-C1eqZk1M.cjs → data-inventory-BKAQGjFN.cjs} +2 -2
- package/dist/{data-inventory-C1eqZk1M.cjs.map → data-inventory-BKAQGjFN.cjs.map} +1 -1
- package/dist/{dataFlowsToDataSilos-DXlFFHMV.cjs → dataFlowsToDataSilos-CnvG2jqy.cjs} +2 -2
- package/dist/{dataFlowsToDataSilos-DXlFFHMV.cjs.map → dataFlowsToDataSilos-CnvG2jqy.cjs.map} +1 -1
- package/dist/{impl-BOEoFzcB.cjs → impl-1-1sg4WF.cjs} +2 -2
- package/dist/{impl-BOEoFzcB.cjs.map → impl-1-1sg4WF.cjs.map} +1 -1
- package/dist/{impl-DwWoAbT_.cjs → impl-57HOh2c3.cjs} +2 -2
- package/dist/{impl-DwWoAbT_.cjs.map → impl-57HOh2c3.cjs.map} +1 -1
- package/dist/{impl-B_2CdctV.cjs → impl-58WnFNmn.cjs} +2 -2
- package/dist/{impl-B_2CdctV.cjs.map → impl-58WnFNmn.cjs.map} +1 -1
- package/dist/{impl-BOEjB3fo.cjs → impl-B4OVz7FC.cjs} +2 -2
- package/dist/{impl-BOEjB3fo.cjs.map → impl-B4OVz7FC.cjs.map} +1 -1
- package/dist/{impl-BfC5CRRX.cjs → impl-BFRrE04X.cjs} +2 -2
- package/dist/{impl-BfC5CRRX.cjs.map → impl-BFRrE04X.cjs.map} +1 -1
- package/dist/{impl-DqfyWyoV.cjs → impl-BSS_avMv.cjs} +2 -2
- package/dist/{impl-DqfyWyoV.cjs.map → impl-BSS_avMv.cjs.map} +1 -1
- package/dist/{impl-DNRsFfbU.cjs → impl-BVmw0mE4.cjs} +2 -2
- package/dist/{impl-DNRsFfbU.cjs.map → impl-BVmw0mE4.cjs.map} +1 -1
- package/dist/{impl-C6JjApDI.cjs → impl-Bf_hLViY.cjs} +2 -2
- package/dist/{impl-C6JjApDI.cjs.map → impl-Bf_hLViY.cjs.map} +1 -1
- package/dist/{impl-DxhyqjcY.cjs → impl-BkEg-Nm6.cjs} +2 -2
- package/dist/{impl-DxhyqjcY.cjs.map → impl-BkEg-Nm6.cjs.map} +1 -1
- package/dist/{impl-7LAuV25D.cjs → impl-Bmln6D88.cjs} +2 -2
- package/dist/{impl-7LAuV25D.cjs.map → impl-Bmln6D88.cjs.map} +1 -1
- package/dist/{impl-DhbV3bBZ.cjs → impl-BqIqzp40.cjs} +2 -2
- package/dist/{impl-DhbV3bBZ.cjs.map → impl-BqIqzp40.cjs.map} +1 -1
- package/dist/{impl-u8o3S8w2.cjs → impl-BszlCtcR.cjs} +2 -2
- package/dist/{impl-u8o3S8w2.cjs.map → impl-BszlCtcR.cjs.map} +1 -1
- package/dist/{impl-DetfC7CT.cjs → impl-BtuKKdl3.cjs} +2 -2
- package/dist/{impl-DetfC7CT.cjs.map → impl-BtuKKdl3.cjs.map} +1 -1
- package/dist/{impl-_QrpPIPw.cjs → impl-C2e4xVvX.cjs} +2 -2
- package/dist/{impl-_QrpPIPw.cjs.map → impl-C2e4xVvX.cjs.map} +1 -1
- package/dist/{impl-CyzGdwB1.cjs → impl-C65nk0G8.cjs} +2 -2
- package/dist/{impl-CyzGdwB1.cjs.map → impl-C65nk0G8.cjs.map} +1 -1
- package/dist/{impl-DjP2MJNK.cjs → impl-CCdxbRmg.cjs} +2 -2
- package/dist/{impl-DjP2MJNK.cjs.map → impl-CCdxbRmg.cjs.map} +1 -1
- package/dist/{impl-DmXYpp-M.cjs → impl-CMwmo2vR.cjs} +2 -2
- package/dist/{impl-DmXYpp-M.cjs.map → impl-CMwmo2vR.cjs.map} +1 -1
- package/dist/{impl-CR-wyJSg.cjs → impl-Cb64HwGx.cjs} +2 -2
- package/dist/{impl-CR-wyJSg.cjs.map → impl-Cb64HwGx.cjs.map} +1 -1
- package/dist/{impl-CV3axMeT.cjs → impl-CdfA8kxo.cjs} +2 -2
- package/dist/{impl-CV3axMeT.cjs.map → impl-CdfA8kxo.cjs.map} +1 -1
- package/dist/{impl-C-aKX3zu.cjs → impl-CkfOZzpI.cjs} +2 -2
- package/dist/{impl-C-aKX3zu.cjs.map → impl-CkfOZzpI.cjs.map} +1 -1
- package/dist/{impl-CKYwKeLz.cjs → impl-ClujxTb8.cjs} +2 -2
- package/dist/{impl-CKYwKeLz.cjs.map → impl-ClujxTb8.cjs.map} +1 -1
- package/dist/{impl-B04CctrY.cjs → impl-D-IWtHQi.cjs} +2 -2
- package/dist/{impl-B04CctrY.cjs.map → impl-D-IWtHQi.cjs.map} +1 -1
- package/dist/{impl-BGoAnVJu.cjs → impl-D9-ZQmJB.cjs} +2 -2
- package/dist/{impl-BGoAnVJu.cjs.map → impl-D9-ZQmJB.cjs.map} +1 -1
- package/dist/{impl-CmEsmnYZ.cjs → impl-DGel0ZLe.cjs} +2 -2
- package/dist/{impl-CmEsmnYZ.cjs.map → impl-DGel0ZLe.cjs.map} +1 -1
- package/dist/{impl-DHuguAlW.cjs → impl-DL2j8g1C.cjs} +2 -2
- package/dist/{impl-DHuguAlW.cjs.map → impl-DL2j8g1C.cjs.map} +1 -1
- package/dist/{impl-LgUGDTQK.cjs → impl-DSNgFKP_.cjs} +2 -2
- package/dist/{impl-LgUGDTQK.cjs.map → impl-DSNgFKP_.cjs.map} +1 -1
- package/dist/{impl-Dzq0t6mX.cjs → impl-DU85U1jO.cjs} +2 -2
- package/dist/{impl-Dzq0t6mX.cjs.map → impl-DU85U1jO.cjs.map} +1 -1
- package/dist/{impl-C4q9xHFr.cjs → impl-DV5f54rm.cjs} +2 -2
- package/dist/{impl-C4q9xHFr.cjs.map → impl-DV5f54rm.cjs.map} +1 -1
- package/dist/{impl-CLcnbVfj.cjs → impl-DXKJH0AZ.cjs} +2 -2
- package/dist/{impl-CLcnbVfj.cjs.map → impl-DXKJH0AZ.cjs.map} +1 -1
- package/dist/{impl-6TmoWv0o.cjs → impl-DbxzDk8h.cjs} +2 -2
- package/dist/{impl-6TmoWv0o.cjs.map → impl-DbxzDk8h.cjs.map} +1 -1
- package/dist/{impl-XyWPUpvw.cjs → impl-DhnCAbU-.cjs} +2 -2
- package/dist/{impl-XyWPUpvw.cjs.map → impl-DhnCAbU-.cjs.map} +1 -1
- package/dist/{impl-kxwq3OMk.cjs → impl-Dj2fTDNO.cjs} +2 -2
- package/dist/{impl-kxwq3OMk.cjs.map → impl-Dj2fTDNO.cjs.map} +1 -1
- package/dist/{impl-Cp7-Tctr.cjs → impl-KAorCmlT.cjs} +2 -2
- package/dist/{impl-Cp7-Tctr.cjs.map → impl-KAorCmlT.cjs.map} +1 -1
- package/dist/{impl-CnRqR4kw.cjs → impl-LMp29vxd.cjs} +2 -2
- package/dist/{impl-CnRqR4kw.cjs.map → impl-LMp29vxd.cjs.map} +1 -1
- package/dist/{impl-DC_YquN8.cjs → impl-MrsSr72p.cjs} +2 -2
- package/dist/{impl-DC_YquN8.cjs.map → impl-MrsSr72p.cjs.map} +1 -1
- package/dist/{impl-CgKn47V9.cjs → impl-SZp3iTUp.cjs} +2 -2
- package/dist/{impl-CgKn47V9.cjs.map → impl-SZp3iTUp.cjs.map} +1 -1
- package/dist/{impl-DrJj-l3s.cjs → impl-W6jE_UV0.cjs} +2 -2
- package/dist/{impl-DrJj-l3s.cjs.map → impl-W6jE_UV0.cjs.map} +1 -1
- package/dist/{impl-Dp3-sA6b.cjs → impl-XwC7A99P.cjs} +2 -2
- package/dist/{impl-Dp3-sA6b.cjs.map → impl-XwC7A99P.cjs.map} +1 -1
- package/dist/{impl-DQ8rr7Fv.cjs → impl-ebVxRYAc.cjs} +2 -2
- package/dist/{impl-DQ8rr7Fv.cjs.map → impl-ebVxRYAc.cjs.map} +1 -1
- package/dist/{impl-CsKfLxov.cjs → impl-k61p_VQY.cjs} +2 -2
- package/dist/{impl-CsKfLxov.cjs.map → impl-k61p_VQY.cjs.map} +1 -1
- package/dist/{impl-CqadSQOh.cjs → impl-kMebV10f.cjs} +2 -2
- package/dist/{impl-CqadSQOh.cjs.map → impl-kMebV10f.cjs.map} +1 -1
- package/dist/{impl-Dvoj_snk.cjs → impl-oYFKp06U.cjs} +2 -2
- package/dist/{impl-Dvoj_snk.cjs.map → impl-oYFKp06U.cjs.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +9 -9
- package/dist/{manual-enrichment-CzTpv-mM.cjs → manual-enrichment-C6h9gjY1.cjs} +2 -2
- package/dist/{manual-enrichment-CzTpv-mM.cjs.map → manual-enrichment-C6h9gjY1.cjs.map} +1 -1
- package/dist/{pooling-DA_LwUEp.cjs → pooling-DLEGcLtt.cjs} +5 -5
- package/dist/pooling-DLEGcLtt.cjs.map +1 -0
- package/dist/{preference-management-aOhuZCuE.cjs → preference-management-B36PQuMK.cjs} +2 -2
- package/dist/{preference-management-aOhuZCuE.cjs.map → preference-management-B36PQuMK.cjs.map} +1 -1
- package/dist/{syncConfigurationToTranscend-DuTZKIG8.cjs → syncConfigurationToTranscend-DKliAJhK.cjs} +2 -2
- package/dist/{syncConfigurationToTranscend-DuTZKIG8.cjs.map → syncConfigurationToTranscend-DKliAJhK.cjs.map} +1 -1
- package/dist/{uploadConsents-C9Pv8Awr.cjs → uploadConsents-MtgCk8B0.cjs} +2 -2
- package/dist/{uploadConsents-C9Pv8Awr.cjs.map → uploadConsents-MtgCk8B0.cjs.map} +1 -1
- package/package.json +1 -1
- package/dist/pooling-DA_LwUEp.cjs.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require(`./chunk-Bmb41Sf3.cjs`),t=require(`./constants-
|
|
2
|
-
//# sourceMappingURL=manual-enrichment-
|
|
1
|
+
const e=require(`./chunk-Bmb41Sf3.cjs`),t=require(`./constants-K6pQQtc7.cjs`),n=require(`./syncConfigurationToTranscend-DKliAJhK.cjs`),r=require(`./logger-DQwEYtSS.cjs`);let i=require(`@transcend-io/privacy-types`),a=require(`colors`);a=e.t(a);let o=require(`io-ts`);o=e.t(o);async function s({file:e,auth:o,sombraAuth:s,requestActions:c=[],concurrency:l=100,transcendUrl:u=t.a}){let d=n.ti(u,o),f=await n.ei(u,o,s);r.t.info(a.default.magenta(`Pulling manual enrichment requests, filtered for actions: ${c.join(`,`)}`));let p=await n.fr(d,{actions:c,statuses:[i.RequestStatus.Enriching]}),m=[];await n.Ts(p,async e=>{let t=await n.gr(d,{requestId:e.id});if(t.filter(({status:e})=>e===`ACTION_REQUIRED`)){let r=await n.mr(d,f,{requestId:e.id});m.push({...e,requestIdentifiers:r,requestEnrichers:t})}},{concurrency:l});let h=m.map(({attributeValues:e,requestIdentifiers:t,requestEnrichers:r,...i})=>({...i,...Object.entries(n.As(t,`name`)).reduce((e,[t,n])=>Object.assign(e,{[t]:n.map(({value:e})=>e).join(`,`)}),{}),...Object.entries(n.As(e,`attributeKey.name`)).reduce((e,[t,n])=>Object.assign(e,{[t]:n.map(({name:e})=>e).join(`,`)}),{})}));return await n.l(e,h,n.Ds(h.map(e=>Object.keys(e)).flat())),r.t.info(a.default.green(`Successfully wrote ${m.length} requests to file "${e}"`)),m}const c=`https://app.transcend.io/privacy-requests/incoming-requests/`,l=o.record(o.string,o.string);async function u(e,{id:t,...i},o,s){if(!t){let e=`Request ID must be provided to enricher request.${s?` Found error in row: ${s}`:``}`;throw r.t.error(a.default.red(e)),Error(e)}let l=t.toLowerCase(),u=Object.entries(i).reduce((e,[t,r])=>n.Ds(n.li(r)).length===0?e:Object.assign(e,{[t]:n.Ds(n.li(r)).map(e=>({value:t===`email`?e.toLowerCase():e}))}),{});try{return await e.post(`v1/enrich-identifiers`,{headers:{"x-transcend-request-id":l,"x-transcend-enricher-id":o},json:{enrichedIdentifiers:u}}).json(),r.t.error(a.default.green(`Successfully enriched request: ${c}${l}`)),!0}catch(e){if(typeof e.response.body==`string`&&e.response.body.includes(`Cannot update a resolved RequestEnricher`))return r.t.warn(a.default.magenta(`Skipped enrichment for request: ${c}${l}, request is no longer in the enriching phase.`)),!1;throw r.t.error(a.default.red(`Failed to enricher identifiers for request with id: ${c}${l} - ${e.message} - ${e.response.body}`)),e}}async function d({file:e,auth:i,sombraAuth:o,enricherId:s,markSilent:c,concurrency:d=100,transcendUrl:f=t.a}){let p=await n.ei(f,i,o),m=n.ti(f,i);r.t.info(a.default.magenta(`Reading "${e}" from disk`));let h=n.oi(e,l);r.t.info(a.default.magenta(`Enriching "${h.length}" privacy requests.`));let g=0,_=0,v=0;if(await n.Ts(h,async(e,t)=>{try{c&&(await n.i(m,n.Ao,{input:{id:e.id,isSilent:!0}}),r.t.info(a.default.magenta(`Mark request as silent mode - ${e.id}`))),await u(p,e,s,t)?g+=1:_+=1}catch{v+=1}},{concurrency:d}),r.t.info(a.default.green(`Successfully notified Transcend! \n Success count: ${g}.`)),_>0&&r.t.info(a.default.magenta(`Skipped count: ${_}.`)),v>0)throw r.t.info(a.default.red(`Error Count: ${v}.`)),Error(`Failed to enrich: ${v} requests.`);return h.length}Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return d}});
|
|
2
|
+
//# sourceMappingURL=manual-enrichment-C6h9gjY1.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manual-enrichment-CzTpv-mM.cjs","names":["DEFAULT_TRANSCEND_API","buildTranscendGraphQLClient","createSombraGotInstance","fetchAllRequests","RequestStatus","map","fetchAllRequestEnrichers","fetchAllRequestIdentifiers","groupBy","writeCsv","uniq","t","uniq","splitCsvToList","DEFAULT_TRANSCEND_API","createSombraGotInstance","buildTranscendGraphQLClient","readCsv","map","makeGraphQLRequest","UPDATE_PRIVACY_REQUEST"],"sources":["../src/lib/manual-enrichment/pullManualEnrichmentIdentifiersToCsv.ts","../src/lib/manual-enrichment/enrichPrivacyRequest.ts","../src/lib/manual-enrichment/pushManualEnrichmentIdentifiersFromCsv.ts"],"sourcesContent":["import { RequestAction, RequestStatus } from '@transcend-io/privacy-types';\nimport { map } from '../bluebird';\nimport colors from 'colors';\nimport { groupBy, uniq } from 'lodash-es';\nimport { DEFAULT_TRANSCEND_API } from '../../constants';\nimport { writeCsv } from '../helpers/writeCsv';\nimport {\n PrivacyRequest,\n RequestEnricher,\n RequestIdentifier,\n buildTranscendGraphQLClient,\n createSombraGotInstance,\n fetchAllRequestEnrichers,\n fetchAllRequestIdentifiers,\n fetchAllRequests,\n} from '../graphql';\nimport { logger } from '../../logger';\n\nexport interface PrivacyRequestWithIdentifiers extends PrivacyRequest {\n /** Request Enrichers */\n requestEnrichers: RequestEnricher[];\n /** Request Identifiers */\n requestIdentifiers: RequestIdentifier[];\n}\n\n/**\n * Pull the set of manual enrichment jobs to CSV\n *\n * @param options - Options\n * @returns List of requests with identifiers\n */\nexport async function pullManualEnrichmentIdentifiersToCsv({\n file,\n auth,\n sombraAuth,\n requestActions = [],\n concurrency = 100,\n transcendUrl = DEFAULT_TRANSCEND_API,\n}: {\n /** CSV file path */\n file: string;\n /** Transcend API key authentication */\n auth: string;\n /** Sombra API key */\n sombraAuth?: string;\n /** Concurrency */\n concurrency?: number;\n /** The request actions to fetch */\n requestActions?: RequestAction[];\n /** API URL for Transcend backend */\n transcendUrl?: string;\n}): Promise<PrivacyRequestWithIdentifiers[]> {\n // Find all requests made before createdAt that are in a removing data state\n const client = buildTranscendGraphQLClient(transcendUrl, auth);\n const sombra = await createSombraGotInstance(transcendUrl, auth, sombraAuth);\n\n logger.info(\n colors.magenta(\n `Pulling manual enrichment requests, filtered for actions: ${requestActions.join(\n ',',\n )}`,\n ),\n );\n\n // Pull all privacy requests\n const allRequests = await fetchAllRequests(client, {\n actions: requestActions,\n statuses: [RequestStatus.Enriching],\n });\n\n // Requests to save\n const savedRequests: PrivacyRequestWithIdentifiers[] = [];\n\n // Filter down requests to what is needed\n await map(\n allRequests,\n async (request) => {\n // Fetch enrichers\n const requestEnrichers = await fetchAllRequestEnrichers(client, {\n requestId: request.id,\n });\n\n // Check if manual enrichment exists for that request\n const hasManualEnrichment = requestEnrichers.filter(\n ({ status }) => status === 'ACTION_REQUIRED',\n );\n\n // Save request to queue\n if (hasManualEnrichment) {\n const requestIdentifiers = await fetchAllRequestIdentifiers(\n client,\n sombra,\n {\n requestId: request.id,\n },\n );\n savedRequests.push({\n ...request,\n requestIdentifiers,\n requestEnrichers,\n });\n }\n },\n {\n concurrency,\n },\n );\n\n const data = savedRequests.map(\n ({\n attributeValues,\n requestIdentifiers,\n requestEnrichers, // eslint-disable-line @typescript-eslint/no-unused-vars\n ...request\n }) => ({\n ...request,\n // flatten identifiers\n ...Object.entries(groupBy(requestIdentifiers, 'name')).reduce(\n (acc, [key, values]) =>\n Object.assign(acc, {\n [key]: values.map(({ value }) => value).join(','),\n }),\n {},\n ),\n // flatten attributes\n ...Object.entries(groupBy(attributeValues, 'attributeKey.name')).reduce(\n (acc, [key, values]) =>\n Object.assign(acc, {\n [key]: values.map(({ name }) => name).join(','),\n }),\n {},\n ),\n }),\n );\n\n // Write out to CSV\n const headers = uniq(data.map((d) => Object.keys(d)).flat());\n await writeCsv(file, data, headers);\n\n logger.info(\n colors.green(\n `Successfully wrote ${savedRequests.length} requests to file \"${file}\"`,\n ),\n );\n\n return savedRequests;\n}\n","import type { Got } from 'got';\nimport * as t from 'io-ts';\nimport { logger } from '../../logger';\nimport { uniq } from 'lodash-es';\nimport colors from 'colors';\nimport { splitCsvToList } from '../requests/splitCsvToList';\n\nconst ADMIN_URL =\n 'https://app.transcend.io/privacy-requests/incoming-requests/';\n/**\n * Minimal set required to mark as completed\n */\nexport const EnrichPrivacyRequest = t.record(t.string, t.string);\n\n/** Type override */\nexport type EnrichPrivacyRequest = t.TypeOf<typeof EnrichPrivacyRequest>;\n\n/**\n * Upload identifiers to a privacy request or mark request as\n *\n * @param sombra - Sombra instance configured to make requests\n * @param request - Request to enricher\n * @param enricherId - The ID of the enricher being uploaded to\n * @param index - Index of request ID\n * @returns True if enriched successfully, false if skipped, throws error if failed\n */\nexport async function enrichPrivacyRequest(\n sombra: Got,\n { id: rawId, ...rest }: EnrichPrivacyRequest,\n enricherId: string,\n index?: number,\n): Promise<boolean> {\n if (!rawId) {\n // error\n const msg = `Request ID must be provided to enricher request.${\n index ? ` Found error in row: ${index}` : ''\n }`;\n logger.error(colors.red(msg));\n throw new Error(msg);\n }\n\n const id = rawId.toLowerCase();\n\n // Pull out the identifiers\n const enrichedIdentifiers = Object.entries(rest).reduce(\n (acc, [key, value]) => {\n const values = uniq(splitCsvToList(value));\n return values.length === 0\n ? acc\n : Object.assign(acc, {\n [key]: uniq(splitCsvToList(value)).map((val) => ({\n value: key === 'email' ? val.toLowerCase() : val,\n })),\n });\n },\n {} as Record<string, string[]>,\n );\n\n // Make the GraphQL request\n try {\n await sombra\n .post('v1/enrich-identifiers', {\n headers: {\n 'x-transcend-request-id': id,\n 'x-transcend-enricher-id': enricherId,\n },\n json: {\n enrichedIdentifiers,\n },\n })\n .json();\n\n logger.error(\n colors.green(`Successfully enriched request: ${ADMIN_URL}${id}`),\n );\n return true;\n } catch (err) {\n // skip if already enriched\n if (\n typeof err.response.body === 'string' &&\n err.response.body.includes('Cannot update a resolved RequestEnricher')\n ) {\n logger.warn(\n colors.magenta(\n `Skipped enrichment for request: ${ADMIN_URL}${id}, request is no longer in the enriching phase.`,\n ),\n );\n return false;\n }\n\n // error\n logger.error(\n colors.red(\n `Failed to enricher identifiers for request with id: ${ADMIN_URL}${id} - ${err.message} - ${err.response.body}`,\n ),\n );\n throw err;\n }\n}\n","import colors from 'colors';\nimport { map } from '../bluebird';\nimport { logger } from '../../logger';\nimport {\n UPDATE_PRIVACY_REQUEST,\n buildTranscendGraphQLClient,\n createSombraGotInstance,\n makeGraphQLRequest,\n} from '../graphql';\nimport {\n enrichPrivacyRequest,\n EnrichPrivacyRequest,\n} from './enrichPrivacyRequest';\nimport { readCsv } from '../requests';\nimport { DEFAULT_TRANSCEND_API } from '../../constants';\n\n/**\n * Push a CSV of enriched requests back into Transcend\n *\n * @param options - Options\n * @returns Number of items processed\n */\nexport async function pushManualEnrichmentIdentifiersFromCsv({\n file,\n auth,\n sombraAuth,\n enricherId,\n markSilent,\n concurrency = 100,\n transcendUrl = DEFAULT_TRANSCEND_API,\n}: {\n /** CSV file path */\n file: string;\n /** Transcend API key authentication */\n auth: string;\n /** ID of enricher being uploaded to */\n enricherId: string;\n /** Sombra API key authentication */\n sombraAuth?: string;\n /** Concurrency */\n concurrency?: number;\n /** API URL for Transcend backend */\n transcendUrl?: string;\n /** Mark requests in silent mode before enriching */\n markSilent?: boolean;\n}): Promise<number> {\n // Create sombra instance to communicate with\n const sombra = await createSombraGotInstance(transcendUrl, auth, sombraAuth);\n const client = buildTranscendGraphQLClient(transcendUrl, auth);\n\n // Read from CSV\n logger.info(colors.magenta(`Reading \"${file}\" from disk`));\n const activeResults = readCsv(file, EnrichPrivacyRequest);\n\n // Notify Transcend\n logger.info(\n colors.magenta(`Enriching \"${activeResults.length}\" privacy requests.`),\n );\n\n let successCount = 0;\n let skippedCount = 0;\n let errorCount = 0;\n\n await map(\n activeResults,\n async (request, index) => {\n try {\n // Mark requests in silent mode before a certain date\n if (markSilent) {\n await makeGraphQLRequest(client, UPDATE_PRIVACY_REQUEST, {\n input: {\n id: request.id,\n isSilent: true,\n },\n });\n\n logger.info(\n colors.magenta(`Mark request as silent mode - ${request.id}`),\n );\n }\n\n const result = await enrichPrivacyRequest(\n sombra,\n request,\n enricherId,\n index,\n );\n if (result) {\n successCount += 1;\n } else {\n skippedCount += 1;\n }\n } catch (err) {\n errorCount += 1;\n }\n },\n { concurrency },\n );\n\n logger.info(\n colors.green(\n `Successfully notified Transcend! \\n Success count: ${successCount}.`,\n ),\n );\n\n if (skippedCount > 0) {\n logger.info(colors.magenta(`Skipped count: ${skippedCount}.`));\n }\n\n if (errorCount > 0) {\n logger.info(colors.red(`Error Count: ${errorCount}.`));\n throw new Error(`Failed to enrich: ${errorCount} requests.`);\n }\n\n return activeResults.length;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"manual-enrichment-C6h9gjY1.cjs","names":["DEFAULT_TRANSCEND_API","buildTranscendGraphQLClient","createSombraGotInstance","fetchAllRequests","RequestStatus","map","fetchAllRequestEnrichers","fetchAllRequestIdentifiers","groupBy","writeCsv","uniq","t","uniq","splitCsvToList","DEFAULT_TRANSCEND_API","createSombraGotInstance","buildTranscendGraphQLClient","readCsv","map","makeGraphQLRequest","UPDATE_PRIVACY_REQUEST"],"sources":["../src/lib/manual-enrichment/pullManualEnrichmentIdentifiersToCsv.ts","../src/lib/manual-enrichment/enrichPrivacyRequest.ts","../src/lib/manual-enrichment/pushManualEnrichmentIdentifiersFromCsv.ts"],"sourcesContent":["import { RequestAction, RequestStatus } from '@transcend-io/privacy-types';\nimport { map } from '../bluebird';\nimport colors from 'colors';\nimport { groupBy, uniq } from 'lodash-es';\nimport { DEFAULT_TRANSCEND_API } from '../../constants';\nimport { writeCsv } from '../helpers/writeCsv';\nimport {\n PrivacyRequest,\n RequestEnricher,\n RequestIdentifier,\n buildTranscendGraphQLClient,\n createSombraGotInstance,\n fetchAllRequestEnrichers,\n fetchAllRequestIdentifiers,\n fetchAllRequests,\n} from '../graphql';\nimport { logger } from '../../logger';\n\nexport interface PrivacyRequestWithIdentifiers extends PrivacyRequest {\n /** Request Enrichers */\n requestEnrichers: RequestEnricher[];\n /** Request Identifiers */\n requestIdentifiers: RequestIdentifier[];\n}\n\n/**\n * Pull the set of manual enrichment jobs to CSV\n *\n * @param options - Options\n * @returns List of requests with identifiers\n */\nexport async function pullManualEnrichmentIdentifiersToCsv({\n file,\n auth,\n sombraAuth,\n requestActions = [],\n concurrency = 100,\n transcendUrl = DEFAULT_TRANSCEND_API,\n}: {\n /** CSV file path */\n file: string;\n /** Transcend API key authentication */\n auth: string;\n /** Sombra API key */\n sombraAuth?: string;\n /** Concurrency */\n concurrency?: number;\n /** The request actions to fetch */\n requestActions?: RequestAction[];\n /** API URL for Transcend backend */\n transcendUrl?: string;\n}): Promise<PrivacyRequestWithIdentifiers[]> {\n // Find all requests made before createdAt that are in a removing data state\n const client = buildTranscendGraphQLClient(transcendUrl, auth);\n const sombra = await createSombraGotInstance(transcendUrl, auth, sombraAuth);\n\n logger.info(\n colors.magenta(\n `Pulling manual enrichment requests, filtered for actions: ${requestActions.join(\n ',',\n )}`,\n ),\n );\n\n // Pull all privacy requests\n const allRequests = await fetchAllRequests(client, {\n actions: requestActions,\n statuses: [RequestStatus.Enriching],\n });\n\n // Requests to save\n const savedRequests: PrivacyRequestWithIdentifiers[] = [];\n\n // Filter down requests to what is needed\n await map(\n allRequests,\n async (request) => {\n // Fetch enrichers\n const requestEnrichers = await fetchAllRequestEnrichers(client, {\n requestId: request.id,\n });\n\n // Check if manual enrichment exists for that request\n const hasManualEnrichment = requestEnrichers.filter(\n ({ status }) => status === 'ACTION_REQUIRED',\n );\n\n // Save request to queue\n if (hasManualEnrichment) {\n const requestIdentifiers = await fetchAllRequestIdentifiers(\n client,\n sombra,\n {\n requestId: request.id,\n },\n );\n savedRequests.push({\n ...request,\n requestIdentifiers,\n requestEnrichers,\n });\n }\n },\n {\n concurrency,\n },\n );\n\n const data = savedRequests.map(\n ({\n attributeValues,\n requestIdentifiers,\n requestEnrichers, // eslint-disable-line @typescript-eslint/no-unused-vars\n ...request\n }) => ({\n ...request,\n // flatten identifiers\n ...Object.entries(groupBy(requestIdentifiers, 'name')).reduce(\n (acc, [key, values]) =>\n Object.assign(acc, {\n [key]: values.map(({ value }) => value).join(','),\n }),\n {},\n ),\n // flatten attributes\n ...Object.entries(groupBy(attributeValues, 'attributeKey.name')).reduce(\n (acc, [key, values]) =>\n Object.assign(acc, {\n [key]: values.map(({ name }) => name).join(','),\n }),\n {},\n ),\n }),\n );\n\n // Write out to CSV\n const headers = uniq(data.map((d) => Object.keys(d)).flat());\n await writeCsv(file, data, headers);\n\n logger.info(\n colors.green(\n `Successfully wrote ${savedRequests.length} requests to file \"${file}\"`,\n ),\n );\n\n return savedRequests;\n}\n","import type { Got } from 'got';\nimport * as t from 'io-ts';\nimport { logger } from '../../logger';\nimport { uniq } from 'lodash-es';\nimport colors from 'colors';\nimport { splitCsvToList } from '../requests/splitCsvToList';\n\nconst ADMIN_URL =\n 'https://app.transcend.io/privacy-requests/incoming-requests/';\n/**\n * Minimal set required to mark as completed\n */\nexport const EnrichPrivacyRequest = t.record(t.string, t.string);\n\n/** Type override */\nexport type EnrichPrivacyRequest = t.TypeOf<typeof EnrichPrivacyRequest>;\n\n/**\n * Upload identifiers to a privacy request or mark request as\n *\n * @param sombra - Sombra instance configured to make requests\n * @param request - Request to enricher\n * @param enricherId - The ID of the enricher being uploaded to\n * @param index - Index of request ID\n * @returns True if enriched successfully, false if skipped, throws error if failed\n */\nexport async function enrichPrivacyRequest(\n sombra: Got,\n { id: rawId, ...rest }: EnrichPrivacyRequest,\n enricherId: string,\n index?: number,\n): Promise<boolean> {\n if (!rawId) {\n // error\n const msg = `Request ID must be provided to enricher request.${\n index ? ` Found error in row: ${index}` : ''\n }`;\n logger.error(colors.red(msg));\n throw new Error(msg);\n }\n\n const id = rawId.toLowerCase();\n\n // Pull out the identifiers\n const enrichedIdentifiers = Object.entries(rest).reduce(\n (acc, [key, value]) => {\n const values = uniq(splitCsvToList(value));\n return values.length === 0\n ? acc\n : Object.assign(acc, {\n [key]: uniq(splitCsvToList(value)).map((val) => ({\n value: key === 'email' ? val.toLowerCase() : val,\n })),\n });\n },\n {} as Record<string, string[]>,\n );\n\n // Make the GraphQL request\n try {\n await sombra\n .post('v1/enrich-identifiers', {\n headers: {\n 'x-transcend-request-id': id,\n 'x-transcend-enricher-id': enricherId,\n },\n json: {\n enrichedIdentifiers,\n },\n })\n .json();\n\n logger.error(\n colors.green(`Successfully enriched request: ${ADMIN_URL}${id}`),\n );\n return true;\n } catch (err) {\n // skip if already enriched\n if (\n typeof err.response.body === 'string' &&\n err.response.body.includes('Cannot update a resolved RequestEnricher')\n ) {\n logger.warn(\n colors.magenta(\n `Skipped enrichment for request: ${ADMIN_URL}${id}, request is no longer in the enriching phase.`,\n ),\n );\n return false;\n }\n\n // error\n logger.error(\n colors.red(\n `Failed to enricher identifiers for request with id: ${ADMIN_URL}${id} - ${err.message} - ${err.response.body}`,\n ),\n );\n throw err;\n }\n}\n","import colors from 'colors';\nimport { map } from '../bluebird';\nimport { logger } from '../../logger';\nimport {\n UPDATE_PRIVACY_REQUEST,\n buildTranscendGraphQLClient,\n createSombraGotInstance,\n makeGraphQLRequest,\n} from '../graphql';\nimport {\n enrichPrivacyRequest,\n EnrichPrivacyRequest,\n} from './enrichPrivacyRequest';\nimport { readCsv } from '../requests';\nimport { DEFAULT_TRANSCEND_API } from '../../constants';\n\n/**\n * Push a CSV of enriched requests back into Transcend\n *\n * @param options - Options\n * @returns Number of items processed\n */\nexport async function pushManualEnrichmentIdentifiersFromCsv({\n file,\n auth,\n sombraAuth,\n enricherId,\n markSilent,\n concurrency = 100,\n transcendUrl = DEFAULT_TRANSCEND_API,\n}: {\n /** CSV file path */\n file: string;\n /** Transcend API key authentication */\n auth: string;\n /** ID of enricher being uploaded to */\n enricherId: string;\n /** Sombra API key authentication */\n sombraAuth?: string;\n /** Concurrency */\n concurrency?: number;\n /** API URL for Transcend backend */\n transcendUrl?: string;\n /** Mark requests in silent mode before enriching */\n markSilent?: boolean;\n}): Promise<number> {\n // Create sombra instance to communicate with\n const sombra = await createSombraGotInstance(transcendUrl, auth, sombraAuth);\n const client = buildTranscendGraphQLClient(transcendUrl, auth);\n\n // Read from CSV\n logger.info(colors.magenta(`Reading \"${file}\" from disk`));\n const activeResults = readCsv(file, EnrichPrivacyRequest);\n\n // Notify Transcend\n logger.info(\n colors.magenta(`Enriching \"${activeResults.length}\" privacy requests.`),\n );\n\n let successCount = 0;\n let skippedCount = 0;\n let errorCount = 0;\n\n await map(\n activeResults,\n async (request, index) => {\n try {\n // Mark requests in silent mode before a certain date\n if (markSilent) {\n await makeGraphQLRequest(client, UPDATE_PRIVACY_REQUEST, {\n input: {\n id: request.id,\n isSilent: true,\n },\n });\n\n logger.info(\n colors.magenta(`Mark request as silent mode - ${request.id}`),\n );\n }\n\n const result = await enrichPrivacyRequest(\n sombra,\n request,\n enricherId,\n index,\n );\n if (result) {\n successCount += 1;\n } else {\n skippedCount += 1;\n }\n } catch (err) {\n errorCount += 1;\n }\n },\n { concurrency },\n );\n\n logger.info(\n colors.green(\n `Successfully notified Transcend! \\n Success count: ${successCount}.`,\n ),\n );\n\n if (skippedCount > 0) {\n logger.info(colors.magenta(`Skipped count: ${skippedCount}.`));\n }\n\n if (errorCount > 0) {\n logger.info(colors.red(`Error Count: ${errorCount}.`));\n throw new Error(`Failed to enrich: ${errorCount} requests.`);\n }\n\n return activeResults.length;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const e=require(`./chunk-Bmb41Sf3.cjs`),t=require(`./constants-
|
|
1
|
+
const e=require(`./chunk-Bmb41Sf3.cjs`),t=require(`./constants-K6pQQtc7.cjs`),n=require(`./syncConfigurationToTranscend-DKliAJhK.cjs`),r=require(`./logger-DQwEYtSS.cjs`);let i=require(`node:fs`),a=require(`node:os`),o=require(`node:path`),s=require(`colors`);s=e.t(s);let c=require(`node:child_process`),l=require(`node:readline`);l=e.t(l);function u(e,t){let n=Math.max(1,a.availableParallelism?.()??1);return{poolSize:typeof e==`number`&&e>0?Math.min(e,t):Math.min(n,t),cpuCount:n}}function d(e){return`'${String(e).replace(/'/g,`'\\''`)}'`}function f(e,t,n=!1){if(n)return;let i=(0,a.platform)();try{if(i===`darwin`){(0,c.spawn)(`osascript`,[`-e`,`
|
|
2
2
|
tell application "Terminal"
|
|
3
3
|
activate
|
|
4
4
|
do script "printf '\\e]0;${t}\\a'; tail -n +1 -f ${e.map(d).join(` -f `)}"
|
|
@@ -14,10 +14,10 @@ Press Esc/Ctrl+] to return to dashboard.
|
|
|
14
14
|
--------------------------------
|
|
15
15
|
|
|
16
16
|
`)}}let x=async e=>{h(`attach()`,`id=${e}`);let t=n.get(e);t&&(g===`attached`&&S(),g=`attached`,_=e,u?.(e),r?.(e),await b(e),v=e=>p.write(e),y=e=>m.write(e),t.stdout?.on(`data`,v),t.stderr?.on(`data`,y),t.once(`exit`,()=>{_===e&&S()}))},S=()=>{if(h(`detach()`,`id=${_}`),_==null)return;let e=_,t=n.get(e);t&&(v&&t.stdout?.off(`data`,v),y&&t.stderr?.off(`data`,y)),v=null,y=null,_=null,g=`dashboard`,i?.()},C=(e,t)=>{h(`keypress`,JSON.stringify({str:e,name:t.name,seq:t.sequence,ctrl:t.ctrl,meta:t.meta,shift:t.shift,mode:g}));let r=E(e,t,g);if(h(`mapped`,JSON.stringify(r)),r)switch(r.type){case`CTRL_C`:if(h(`CTRL_C`),g===`attached`&&_!=null){let e=n.get(_);try{e?.kill(`SIGINT`)}catch{}S();return}a?.();return;case`ATTACH`:if(h(`ATTACH`,`id=${r.id}`,`has=${n.has(r.id)}`),g!==`dashboard`)return;n.has(r.id)&&x(r.id);return;case`CYCLE`:{if(h(`CYCLE`,`delta=${r.delta}`),g!==`dashboard`)return;let e=O(D(n),_,r.delta);e!=null&&x(e);return}case`QUIT`:if(g!==`dashboard`)return;a?.();return;case`DETACH`:h(`DETACH`),g===`attached`&&S();return;case`CTRL_D`:if(g===`attached`&&_!=null){let e=n.get(_);try{e?.stdin?.end()}catch{}}return;case`FORWARD`:if(g===`attached`&&_!=null){let e=n.get(_);try{e?.stdin?.write(r.sequence)}catch{}}}},w=e=>{if(g===`attached`&&_!=null){let t=n.get(_);try{t?.stdin?.write(e)}catch{}}};return f.on(`keypress`,C),f.on(`data`,w),()=>{f.off(`keypress`,C),f.off(`data`,w),f.setRawMode?.(!1),p.write(`\x1B[?25h`)}}let A=``;const j=(e,t)=>{let n=Math.min(e-1,9),r=e<=1?`0`:`0-${n}`,i=e>10?` (Tab/Shift+Tab for ≥10)`:``;return t?s.default.dim(`Run complete — digits to view logs • Tab/Shift+Tab cycle • Esc/Ctrl+] detach • q to quit`):s.default.dim(`Hotkeys: [${r}] attach${i} • e=errors • w=warnings • i=info • l=logs • Tab/Shift+Tab • Esc/Ctrl+] detach • Ctrl+C exit`)};function M(e,t,n=!1){let r=[...t.renderHeader(e),``,...t.renderWorkers(e),...n?[]:[``,j(e.poolSize,e.final)],...t.renderExtras?[``].concat(t.renderExtras(e)):[]].join(`
|
|
17
|
-
`);!e.final&&r===A||(A=r,e.final?process.stdout.write(`\x1B[?25h`):(process.stdout.write(`\x1B[?25l`),l.cursorTo(process.stdout,0,0),l.clearScreenDown(process.stdout)),process.stdout.write(`${r}\n`))}function N(e,t,n){let r=t.get(e);if(x(r))try{let e=b(r);if(e!=null)return e}catch{}return n.get(e)}function P(e){return typeof e==`number`?e.toLocaleString():`0`}function F(e,t=40){let n=Math.max(0,Math.min(100,Math.floor(e))),r=Math.floor(n/100*t);return`█`.repeat(r)+`░`.repeat(t-r)}function I(e){let t=[...e.workerState.values()].filter(e=>e.busy).length,n=e.filesCompleted+e.filesFailed;return{done:n,inProgress:t,pct:e.filesTotal===0?100:Math.floor(n/Math.max(1,e.filesTotal)*100)}}function L(e,t=[]){let{title:n,poolSize:r,cpuCount:i,filesTotal:a,filesCompleted:o,filesFailed:c,throughput:l}=e,{inProgress:u,pct:d}=I(e),f=[`${s.default.bold(n)} — ${r} workers ${s.default.dim(`(CPU avail: ${i})`)}`,`${s.default.dim(`Files`)} ${P(a)} ${s.default.dim(`Completed`)} ${P(o)} ${s.default.dim(`Failed`)} ${c?s.default.red(P(c)):P(c)} ${s.default.dim(`In-flight`)} ${P(u)}`,`[${F(d)}] ${d}%`];if(l){let t=Math.round(l.r10s*3600).toLocaleString(),
|
|
17
|
+
`);!e.final&&r===A||(A=r,e.final?process.stdout.write(`\x1B[?25h`):(process.stdout.write(`\x1B[?25l`),l.cursorTo(process.stdout,0,0),l.clearScreenDown(process.stdout)),process.stdout.write(`${r}\n`))}function N(e,t,n){let r=t.get(e);if(x(r))try{let e=b(r);if(e!=null)return e}catch{}return n.get(e)}function P(e){return typeof e==`number`?e.toLocaleString():`0`}function F(e,t=40){let n=Math.max(0,Math.min(100,Math.floor(e))),r=Math.floor(n/100*t);return`█`.repeat(r)+`░`.repeat(t-r)}function I(e){let t=[...e.workerState.values()].filter(e=>e.busy).length,n=e.filesCompleted+e.filesFailed;return{done:n,inProgress:t,pct:e.filesTotal===0?100:Math.floor(n/Math.max(1,e.filesTotal)*100)}}function L(e,t=[]){let{title:n,poolSize:r,cpuCount:i,filesTotal:a,filesCompleted:o,filesFailed:c,throughput:l}=e,{inProgress:u,pct:d}=I(e),f=[`${s.default.bold(n)} — ${r} workers ${s.default.dim(`(CPU avail: ${i})`)}`,`${s.default.dim(`Files`)} ${P(a)} ${s.default.dim(`Completed`)} ${P(o)} ${s.default.dim(`Failed`)} ${c?s.default.red(P(c)):P(c)} ${s.default.dim(`In-flight`)} ${P(u)}`,`[${F(d)}] ${d}%`];if(l){let t=l.jobsR10s>0||l.jobsR60s>0,n=Math.round((t?l.jobsR10s:l.r10s)*3600).toLocaleString(),r=Math.round((t?l.jobsR60s:l.r60s)*3600).toLocaleString(),i=t?`rec`:`files`,a=e.throughput?.successSoFar==null?``:` Newly uploaded: ${P(e.throughput.successSoFar)}`;f.push(s.default.cyan(`Throughput: ${n} ${i}/hr (1h: ${r} ${i}/hr)${a}`))}return t.length?f.concat(t):f}function R(e,t=e=>e?(0,o.basename)(e):`-`){return[...e.workerState.entries()].map(([e,n])=>{let r=n.lastLevel===`error`?s.default.red(`ERROR `):n.lastLevel===`warn`?s.default.yellow(`WARN `):n.busy?s.default.green(`WORKING`):s.default.dim(`IDLE `),i=t(n.file),a=n.startedAt?`${Math.floor((Date.now()-n.startedAt)/1e3)}s`:`-`,o=n.progress?.processed??0,c=n.progress?.total??0,l=c>0?Math.floor(o/c*100):0;return` [w${e}] ${r} | ${i} | ${a} | [${c>0?F(l,18):` `.repeat(18)}] ${c>0?`${o.toLocaleString()}/${c.toLocaleString()} (${l}%)`:s.default.dim(`—`)}`})}async function z(e){let{title:t,baseDir:r,poolSize:i,cpuCount:a,render:o,childModulePath:c,hooks:l,filesTotal:u,childFlag:d,viewerMode:f=!1}=e,p=e.openLogWindows??!f,m=e.isSilent??!0,v=Date.now(),y=_(r),w=new Map,T=new Map,E=new Map,D=new n.C,O=new n.C,A=new Map,j=l.initTotals?.()??{},M=0,P=0,F=0,I=null,L=!1,R=!1,z=null,B=(e=!1)=>{R||o({title:t,poolSize:i,cpuCount:a,filesTotal:u,filesCompleted:P,filesFailed:F,workerState:T,totals:j,final:e,exportStatus:l.exportStatus?.(),throughput:{successSoFar:P,r10s:D.rate(1e4),r60s:D.rate(6e4),jobsR10s:O.rate(1e4),jobsR60s:O.rate(6e4)}})},V=e=>{let t=l.nextTask();if(!t)return!1;let n=w.get(e),r=l.taskLabel(t),i=l.initSlotProgress?.(t);return T.set(e,{busy:!0,file:r,startedAt:Date.now(),lastLevel:`ok`,progress:i}),S(n,{type:`task`,payload:t}),B(),!0};for(let e=0;e<i;e+=1){let t=C({id:e,modulePath:c,logDir:y,openLogWindows:p,isSilent:m,childFlag:d});w.set(e,t),T.set(e,{busy:!1,file:null,startedAt:null,lastLevel:`ok`}),E.set(e,b(t)),M+=1;let n=g(t=>{let n=h(t);if(!n)return;let r=T.get(e);r.lastLevel!==n&&(T.set(e,{...r,lastLevel:n}),B())});t.stderr?.on(`data`,n),t.on(`message`,n=>{if(!(!n||typeof n!=`object`)){if(n.type===`ready`){L||(L=!0,I=setInterval(()=>B(!1),350)),V(e);return}if(n.type===`progress`){j=l.onProgress(j,n.payload);let t=T.get(e);T.set(e,{...t,progress:n.payload});let r=n.payload;if(typeof r?.processed==`number`){let t=A.get(e)??0,n=r.processed-t;n>0&&O.add(n),A.set(e,r.processed)}B();return}if(n.type===`result`){let r=T.get(e),{totals:i,ok:a}=l.onResult(j,n.payload);j=i,a?(P+=1,D.add(1)):F+=1,T.set(e,{...r,busy:!1,file:null,progress:void 0,lastLevel:a?`ok`:`error`}),A.delete(e),!V(e)&&x(t)&&S(t,{type:`shutdown`}),B()}}}),t.on(`exit`,()=>{--M,M===0&&(I&&clearInterval(I),B(!0))})}let H=()=>{},U=()=>{try{process.stdin.setRawMode?.(!1)}catch{}try{process.stdin.pause()}catch{}},W=()=>{if(I&&clearInterval(I),H?.(),z)try{process.stdin.off(`data`,z)}catch{}U(),process.stdout.write(`
|
|
18
18
|
Stopping workers...
|
|
19
|
-
`);for(let[,e]of w){x(e)&&S(e,{type:`shutdown`});try{e?.kill(`SIGTERM`)}catch{}}process.exit(130)},
|
|
20
|
-
`))}process.stdin.resume()}
|
|
19
|
+
`);for(let[,e]of w){x(e)&&S(e,{type:`shutdown`});try{e?.kill(`SIGTERM`)}catch{}}process.exit(130)},G=e=>{R=!0,process.stdout.write(`\x1B[2J\x1B[H`),process.stdout.write(`Attached to worker ${e}. (Esc/Ctrl+] detach • Ctrl+D EOF • Ctrl+C SIGINT)\n`)},K=()=>{R=!1,B()};if(process.once(`SIGINT`,W),!f){if(process.stdin.isTTY){try{process.stdin.setRawMode(!0)}catch{process.stdout.write(s.default.yellow(`Warning: Unable to enable raw mode for interactive key handling.
|
|
20
|
+
`))}process.stdin.resume()}H=k({workers:w,onAttach:G,onDetach:K,onCtrlC:W,getLogPaths:e=>N(e,w,E),replayBytes:200*1024,replayWhich:[`out`,`err`],onEnterAttachScreen:G}),e.extraKeyHandler&&(z=e.extraKeyHandler({logsBySlot:E,repaint:()=>B(),setPaused:e=>{R=e}}),process.stdin.on(`data`,z))}await new Promise(e=>{let t=setInterval(async()=>{if(M===0){if(clearInterval(t),I&&clearInterval(I),H(),z)try{process.stdin.off(`data`,z)}catch{}U();let n=Date.now();try{await l.postProcess?.({slots:T,totals:j,logDir:y,logsBySlot:E,startedAt:v,finishedAt:n,viewerMode:f,getLogPathsForSlot:e=>N(e,w,E)})}catch(e){let t=e?.stack??String(e);process.stdout.write(s.default.red(`postProcess error: ${t}\n`))}e()}},300)})}function B(e){let{logsBySlot:t,repaint:n,setPaused:r,exportMgr:i,exportStatus:a,custom:o}=e,s=e=>{process.stdout.write(`${e}\n`)},c=(e,t)=>{let r=Date.now(),i=a?.[e]??{path:t};a&&(a[e]={path:t||i.path,savedAt:r,exported:!0},n())},l=!1,u=(e,i)=>{l||(l=!0,r(!0),process.stdout.write(`\x1B[2J\x1B[H`),process.stdout.write(`Combined logs viewer (press Esc or Ctrl+] to return)
|
|
21
21
|
|
|
22
22
|
`),(async()=>{try{await w(t,e,i)}catch{l=!1,r(!1),n()}})())},d=(e,n)=>{if(i)try{let r=i.exportCombinedLogs(t,e);s(`\nWrote combined ${n} logs to: ${r}`),c(e,r)}catch{s(`\nFailed to write combined ${n} logs`)}};return e=>{let t=e.toString(`utf8`);if(t===`e`){u([`err`],`error`);return}if(t===`w`){u([`warn`,`err`],`warn`);return}if(t===`i`){u([`info`],`all`);return}if(t===`l`){u([`out`,`err`,`structured`],`all`);return}if(t===`E`){d(`error`,`error`);return}if(t===`W`){d(`warn`,`warn`);return}if(t===`I`){d(`info`,`info`);return}if(t===`A`){d(`all`,`ALL`);return}let i=o?.[t];if(i){i({noteExport:c,say:s});return}(t===`\x1B`||t===``)&&(l=!1,r(!1),n())}}Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return M}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return R}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return z}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return v}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return L}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return B}});
|
|
23
|
-
//# sourceMappingURL=pooling-
|
|
23
|
+
//# sourceMappingURL=pooling-DLEGcLtt.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pooling-DLEGcLtt.cjs","names":["availableParallelism","DEBUG","RateCounter"],"sources":["../src/lib/pooling/computePoolSize.ts","../src/lib/pooling/openTerminal.ts","../src/lib/pooling/ensureLogFile.ts","../src/lib/pooling/logRotation.ts","../src/lib/pooling/spawnWorkerProcess.ts","../src/lib/pooling/showCombinedLogs.ts","../src/lib/pooling/replayFileTailToStdout.ts","../src/lib/pooling/keymap.ts","../src/lib/pooling/workerIds.ts","../src/lib/pooling/installInteractiveSwitcher.ts","../src/lib/pooling/dashboardPlugin.ts","../src/lib/pooling/safeGetLogPathsForSlot.ts","../src/lib/pooling/uiPlugins.ts","../src/lib/pooling/runPool.ts","../src/lib/pooling/createExtraKeyHandler.ts"],"sourcesContent":["import { availableParallelism } from 'node:os';\n\n/**\n * Decide how many worker processes to spawn:\n * - If `concurrency` is set and > 0, use that (capped by file count).\n * - Otherwise, use availableParallelism (capped by file count).\n * Returns both pool size and CPU count for display.\n *\n * @param concurrency - Optional concurrency setting, defaults to undefined\n * @param filesCount - The number of files to process\n * @returns An object with `poolSize` and `cpuCount`\n */\nexport function computePoolSize(\n concurrency: number | undefined,\n filesCount: number,\n): {\n /** The number of worker processes to spawn */\n poolSize: number;\n /** The number of CPU cores available for parallel processing */\n cpuCount: number;\n} {\n const cpuCount = Math.max(1, availableParallelism?.() ?? 1);\n const desired =\n typeof concurrency === 'number' && concurrency > 0\n ? Math.min(concurrency, filesCount)\n : Math.min(cpuCount, filesCount);\n return { poolSize: desired, cpuCount };\n}\n","import colors from 'colors';\nimport { spawn } from 'node:child_process';\nimport { platform } from 'node:os';\nimport { logger } from '../../logger';\n\n/**\n * Escapes a string for use in a shell command.\n *\n * @param p - The string to escape.\n * @returns The escaped string, suitable for use in a shell command.\n */\nexport function shellEscape(p: string): string {\n return `'${String(p).replace(/'/g, \"'\\\\''\")}'`;\n}\n\n/**\n * Opens a new terminal window and tails multiple log files.\n *\n * @param paths - Array of file paths to tail.\n * @param title - Title for the terminal window.\n * @param isSilent - If true, does not open the terminal.\n */\nexport function openLogTailWindowMulti(\n paths: string[],\n title: string,\n isSilent = false,\n): void {\n // If silent mode is enabled, do not open the terminal\n if (isSilent) return;\n\n // Determine the platform and execute the appropriate command\n const p = platform();\n try {\n // For macOS, use AppleScript to open a new Terminal window\n // and tail the specified files\n if (p === 'darwin') {\n const tails = paths.map(shellEscape).join(' -f ');\n const script = `\n tell application \"Terminal\"\n activate\n do script \"printf '\\\\e]0;${title}\\\\a'; tail -n +1 -f ${tails}\"\n end tell\n `;\n spawn('osascript', ['-e', script], { stdio: 'ignore', detached: true });\n return;\n }\n\n // For Windows, use PowerShell to open a new terminal window\n // and tail the specified files\n // The paths are escaped to handle spaces and special characters\n // The command uses Get-Content to tail the files and -Wait to keep the terminal\n if (p === 'win32') {\n const arrayLiteral = `@(${paths\n .map((x) => `'${x.replace(/'/g, \"''\")}'`)\n .join(',')})`;\n const ps = [\n 'powershell',\n '-NoExit',\n '-Command',\n `Write-Host '${title}'; $paths = ${arrayLiteral}; Get-Content -Path $paths -Tail 200 -Wait`,\n ];\n spawn('cmd.exe', ['/c', 'start', ...ps], {\n stdio: 'ignore',\n detached: true,\n }).unref();\n return;\n }\n\n // For Linux, use gnome-terminal or xterm to open a new terminal window\n // and tail the specified files\n // The paths are escaped to handle spaces and special characters\n const tails = paths.map(shellEscape).join(' -f ');\n try {\n spawn(\n 'gnome-terminal',\n [\n '--',\n 'bash',\n '-lc',\n `printf '\\\\e]0;${title}\\\\a'; tail -n +1 -f ${tails}`,\n ],\n {\n stdio: 'ignore',\n detached: true,\n },\n ).unref();\n } catch {\n spawn(\n 'xterm',\n ['-title', title, '-e', `tail -n +1 -f ${paths.join(' ')}`],\n {\n stdio: 'ignore',\n detached: true,\n },\n ).unref();\n }\n } catch (e) {\n logger.error(\n colors.red(\n `Failed to open terminal window for tailing logs: ${\n e instanceof Error ? e.message : String(e)\n }`,\n ),\n );\n throw e;\n }\n}\n","import { closeSync, existsSync, openSync } from 'node:fs';\n\n/**\n * Ensure a log file exists (touch).\n *\n * @param pathStr - the path to the log file\n */\nexport function ensureLogFile(pathStr: string): void {\n if (!existsSync(pathStr)) {\n const fd = openSync(pathStr, 'a');\n closeSync(fd);\n }\n}\n","// logRotation.ts\nimport {\n readdirSync,\n writeFileSync,\n existsSync,\n unlinkSync,\n mkdirSync,\n} from 'node:fs';\nimport { join } from 'node:path';\nimport colors from 'colors';\n\n/**\n * Reset worker logs in the given directory.\n * mode:\n * - \"truncate\": empty files but keep them (best if tails are open)\n * - \"delete\": remove files entirely (simplest if no tails yet)\n *\n * @param dir - Directory to reset logs in\n * @param mode - 'truncate' or 'delete'\n */\nfunction resetWorkerLogs(dir: string, mode: 'truncate' | 'delete'): void {\n const patterns = [\n /worker-\\d+\\.log$/,\n /worker-\\d+\\.out\\.log$/,\n /worker-\\d+\\.err\\.log$/,\n /worker-\\d+\\.warn\\.log$/,\n /worker-\\d+\\.info\\.log$/,\n ];\n for (const name of readdirSync(dir)) {\n // eslint-disable-next-line no-continue\n if (!patterns.some((rx) => rx.test(name))) continue;\n const p = join(dir, name);\n try {\n if (mode === 'delete' && existsSync(p)) unlinkSync(p);\n else writeFileSync(p, '');\n } catch {\n /* ignore */\n }\n }\n process.stdout.write(\n colors.dim(\n `Logs have been ${\n mode === 'delete' ? 'deleted' : 'truncated'\n } in ${dir}\\n`,\n ),\n );\n}\n\n/**\n * Very robust classification of a single log line into warn/error.\n * Returns 'warn' | 'error' | null (null = not a level we care to badge).\n *\n * @param line - Single line of log output to classify\n * @returns 'warn' | 'error' | null\n */\nexport function classifyLogLevel(line: string): 'warn' | 'error' | null {\n // Strip common ANSI sequences\n // eslint-disable-next-line no-control-regex\n const s = line.replace(/\\x1B\\[[0-9;]*m/g, '');\n\n // 1) Explicit worker tag: \"[w12] WARN ...\" or \"[w2] ERROR ...\"\n const mTag = /\\[w\\d+\\]\\s+(ERROR|WARN)\\b/i.exec(s);\n if (mTag) return mTag[1].toLowerCase() as 'warn' | 'error';\n\n // 2) Common plain prefixes\n if (/^\\s*(ERROR|ERR|FATAL)\\b/i.test(s)) return 'error';\n if (/^\\s*(WARN|WARNING)\\b/.test(s)) return 'warn';\n\n // Node runtime warnings\n if (/^\\s*\\(node:\\d+\\)\\s*Warning:/i.test(s)) return 'warn';\n if (/^\\s*DeprecationWarning:/i.test(s)) return 'warn';\n\n // 3) JSON logs (pino/bunyan/etc.)\n // Try to parse as JSON and inspect `level`\n try {\n const j = JSON.parse(s);\n const lv = j?.level;\n if (typeof lv === 'number') {\n // pino levels: 40=warn, 50=error, 60=fatal\n if (lv >= 50) return 'error';\n if (lv >= 40) return 'warn';\n } else if (typeof lv === 'string') {\n const L = lv.toLowerCase();\n if (L === 'error' || L === 'fatal') return 'error';\n if (L === 'warn' || L === 'warning') return 'warn';\n }\n } catch {\n // not JSON, ignore\n }\n\n // 4) Fallthrough: look for level words inside worker-tagged lines\n // e.g. \"[w3] something WARNING xyz\"\n const mInline = /\\[w\\d+\\].*\\b(WARN|WARNING|ERROR|FATAL)\\b/i.exec(s);\n if (mInline) {\n const L = mInline[1].toUpperCase();\n return L === 'ERROR' || L === 'FATAL' ? 'error' : 'warn';\n }\n\n return null;\n}\n\n/**\n * Stream splitter to get whole lines from 'data' events\n *\n * @param onLine - Callback to call with each complete line\n * @returns A function that processes a chunk of data and calls onLine for each complete line\n */\nexport function makeLineSplitter(\n onLine: (line: string) => void,\n): (chunk: Buffer | string) => void {\n let buf = '';\n return (chunk: Buffer | string) => {\n buf += chunk.toString('utf8');\n let nl: number;\n // eslint-disable-next-line no-cond-assign\n while ((nl = buf.indexOf('\\n')) !== -1) {\n const line = buf.slice(0, nl);\n onLine(line);\n buf = buf.slice(nl + 1);\n }\n };\n}\n/**\n * Checks if a log line contains an error indicator.\n *\n * @param t - The log line to check\n * @returns True if the line contains an error keyword, false otherwise\n */\nexport function isLogError(t: string): boolean {\n return /\\b(ERROR|uncaughtException|unhandledRejection)\\b/i.test(t);\n}\n\n/**\n * Checks if a log line contains a warning indicator.\n *\n * @param t - The log line to check\n * @returns True if the line contains a warning keyword, false otherwise\n */\nexport function isLogWarn(t: string): boolean {\n return /\\b(WARN|WARNING)\\b/i.test(t);\n}\n\n/**\n * Determines if a log line is a new header (error, warning, worker tag, or ISO timestamp).\n *\n * @param t - The log line to check\n * @returns True if the line is a new header, false otherwise\n */\nexport function isLogNewHeader(t: string): boolean {\n return (\n isLogError(t) ||\n isLogWarn(t) ||\n /^\\s*\\[w\\d+\\]/.test(t) ||\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/.test(t)\n );\n}\n\n// eslint-disable-next-line no-control-regex\nconst stripAnsi = (s: string): string => s.replace(/\\x1B\\[[0-9;]*m/g, '');\n\n/**\n * Extracts blocks of text from a larger body of text.\n *\n * @param text - The text to extract blocks from\n * @param starts - A function that determines if a line starts a new block\n * @returns An array of extracted blocks\n */\nexport function extractBlocks(\n text: string,\n starts: (cleanLine: string) => boolean,\n): string[] {\n if (!text) return [];\n const out: string[] = [];\n const lines = text.split('\\n');\n let buf: string[] = [];\n let inBlock = false;\n\n const flush = (): void => {\n if (buf.length) out.push(buf.join('\\n'));\n buf = [];\n inBlock = false;\n };\n\n for (const raw of lines) {\n const clean = stripAnsi(raw || '');\n const headery = isLogNewHeader(clean);\n if (!inBlock) {\n if (starts(clean)) {\n inBlock = true;\n buf.push(raw);\n }\n // eslint-disable-next-line no-continue\n continue;\n }\n if (!raw || headery) {\n flush();\n if (starts(clean)) {\n inBlock = true;\n buf.push(raw);\n }\n } else {\n buf.push(raw);\n }\n }\n flush();\n return out.filter(Boolean);\n}\n\n/**\n * The kind of export artifact to retrieve the path for.\n */\nexport type LogExportKind = 'error' | 'warn' | 'info' | 'all';\n\n/**\n * Ensure log directory exists\n *\n * @param rootDir - Root directory\n * @returns log dir\n */\nexport function initLogDir(rootDir: string): string {\n const logDir = join(rootDir, 'logs');\n mkdirSync(logDir, { recursive: true });\n\n const RESET_MODE =\n (process.env.RESET_LOGS as 'truncate' | 'delete') ?? 'truncate';\n resetWorkerLogs(logDir, RESET_MODE);\n\n return logDir;\n}\n\nexport interface ExportArtifactResult {\n /** Whether the artifact was opened successfully */\n ok?: boolean;\n /** The absolute path to the export artifact */\n path: string;\n /** Time saved */\n savedAt?: number;\n /** If exported */\n exported?: boolean;\n}\n\n/**\n * Status map for export artifacts.\n */\nexport type ExportStatusMap = {\n /** The absolute paths to the error log artifacts */\n error?: ExportArtifactResult;\n /** The absolute paths to the warn log artifacts */\n warn?: ExportArtifactResult;\n /** The absolute paths to the info log artifacts */\n info?: ExportArtifactResult;\n /** The absolute paths to all log artifacts */\n all?: ExportArtifactResult;\n /** The absolute paths to the failures CSV artifacts */\n failuresCsv?: ExportArtifactResult;\n};\n\n/**\n * Return export statuses\n *\n * @param logDir - Log directory\n * @returns Export map\n */\nexport function buildExportStatus(logDir: string): ExportStatusMap {\n return {\n error: { path: join(logDir, 'combined-errors.log') },\n warn: { path: join(logDir, 'combined-warns.log') },\n info: { path: join(logDir, 'combined-info.log') },\n all: { path: join(logDir, 'combined-all.log') },\n failuresCsv: { path: join(logDir, 'failing-updates.csv') },\n };\n}\n","import { fork, type ChildProcess } from 'node:child_process';\nimport { join } from 'node:path';\nimport { createWriteStream } from 'node:fs';\nimport { openLogTailWindowMulti } from './openTerminal';\nimport { ensureLogFile } from './ensureLogFile';\nimport { classifyLogLevel, makeLineSplitter } from './logRotation';\n\n/** Default child-flag used if a caller doesn’t provide one. */\nexport const CHILD_FLAG = '--as-child';\n\n// Symbol key so we can stash/retrieve paths on the child proc safely\nconst LOG_PATHS_SYM: unique symbol = Symbol('workerLogPaths');\n\nexport interface WorkerLogPaths {\n /** Structured (app-controlled) log file path written via WORKER_LOG */\n structuredPath: string;\n /** Raw stdout capture */\n outPath: string;\n /** Raw stderr capture */\n errPath: string;\n /** Lines classified as INFO (primarily stdout) */\n infoPath: string;\n /** Lines classified as WARN (from stderr without error tokens) */\n warnPath: string;\n /** Lines classified as ERROR (from stderr, including uncaught) */\n errorPath: string;\n}\n\n/** Convenience alias for the optional return from getWorkerLogPaths */\nexport type SlotPaths = Map<number, WorkerLogPaths | undefined>;\n\n/**\n * Retrieve the paths we stashed on the child.\n *\n * @param child - The worker ChildProcess instance.\n * @returns The log paths or undefined if not set.\n */\nexport function getWorkerLogPaths(\n child: ChildProcess,\n): WorkerLogPaths | undefined {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return (child as any)[LOG_PATHS_SYM] as WorkerLogPaths | undefined;\n}\n\n/**\n * Is IPC channel still open? (Node doesn't type `.channel`)\n *\n * @param w - The worker ChildProcess instance.\n * @returns True if the IPC channel is open, false otherwise.\n */\nexport function isIpcOpen(w: ChildProcess | undefined | null): boolean {\n const ch = w && w.channel;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return !!(w && w.connected && ch && !(ch as any).destroyed);\n}\n\n/**\n * Safely send a message to the worker process.\n *\n * @param w - The worker ChildProcess instance.\n * @param msg - The message to send.\n * @returns True if the message was sent successfully, false otherwise.\n */\nexport function safeSend(w: ChildProcess, msg: unknown): boolean {\n if (!isIpcOpen(w)) return false;\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n w.send?.(msg as any);\n return true;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (err: any) {\n if (\n err?.code === 'ERR_IPC_CHANNEL_CLOSED' ||\n err?.code === 'EPIPE' ||\n err?.errno === -32\n ) {\n return false;\n }\n throw err;\n }\n}\n\nexport interface SpawnWorkerOptions {\n /** Worker slot/index */\n id: number;\n /** Absolute path to the module to fork (should handle CHILD_FLAG) */\n modulePath: string;\n /** Directory where log files will be written */\n logDir: string;\n /** If true, open tail windows for the log files */\n openLogWindows: boolean;\n /** If true, spawn with silent stdio (respect your existing setting) */\n isSilent: boolean;\n /** Optional override for the child flag (defaults to CHILD_FLAG) */\n childFlag?: string;\n}\n\n/**\n * Spawn a worker process with piped stdio and persisted logs.\n *\n * Files produced per worker:\n * - worker-{id}.log (structured WORKER_LOG written by the child)\n * - worker-{id}.out.log (raw stdout)\n * - worker-{id}.err.log (raw stderr)\n * - worker-{id}.info.log (classified INFO lines from stdout)\n * - worker-{id}.warn.log (classified WARN lines from stderr)\n * - worker-{id}.error.log (classified ERROR lines from stderr)\n *\n * @param opts - Options for spawning the worker process.\n * @returns The spawned ChildProcess instance.\n */\nexport function spawnWorkerProcess(opts: SpawnWorkerOptions): ChildProcess {\n const {\n id,\n modulePath,\n logDir,\n openLogWindows,\n isSilent,\n childFlag = CHILD_FLAG,\n } = opts;\n\n const structuredPath = join(logDir, `worker-${id}.log`);\n const outPath = join(logDir, `worker-${id}.out.log`);\n const errPath = join(logDir, `worker-${id}.err.log`);\n const infoPath = join(logDir, `worker-${id}.info.log`);\n const warnPath = join(logDir, `worker-${id}.warn.log`);\n const errorPath = join(logDir, `worker-${id}.error.log`);\n\n [structuredPath, outPath, errPath, infoPath, warnPath, errorPath].forEach(\n ensureLogFile,\n );\n\n const child = fork(modulePath, [childFlag], {\n stdio: ['pipe', 'pipe', 'pipe', 'ipc'],\n env: { ...process.env, WORKER_ID: String(id), WORKER_LOG: structuredPath },\n execArgv: process.execArgv,\n silent: isSilent,\n });\n\n // Raw capture streams\n const outStream = createWriteStream(outPath, { flags: 'a' });\n const errStream = createWriteStream(errPath, { flags: 'a' });\n\n // Classified streams\n const infoStream = createWriteStream(infoPath, { flags: 'a' });\n const warnStream = createWriteStream(warnPath, { flags: 'a' });\n const errorStream = createWriteStream(errorPath, { flags: 'a' });\n\n // Pipe raw streams\n child.stdout?.pipe(outStream);\n child.stderr?.pipe(errStream);\n\n // Headers so tail windows show something immediately\n const hdr = (name: string): string =>\n `[parent] ${name} capture active for w${id} (pid ${child.pid})\\n`;\n outStream.write(hdr('stdout'));\n errStream.write(hdr('stderr'));\n infoStream.write(hdr('info'));\n warnStream.write(hdr('warn'));\n errorStream.write(hdr('error'));\n\n // Classified INFO from stdout (line-buffered)\n if (child.stdout) {\n const onOutLine = makeLineSplitter((line) => {\n if (!line) return;\n try {\n // Treat all stdout lines as INFO for the classified stream\n infoStream.write(`${line}\\n`);\n } catch {\n /* ignore */\n }\n });\n child.stdout.on('data', onOutLine);\n }\n\n // Classified WARN/ERROR from stderr (line-buffered)\n if (child.stderr) {\n const onErrLine = makeLineSplitter((line) => {\n if (!line) return;\n const lvl = classifyLogLevel(line); // 'warn' | 'error' | null\n try {\n if (lvl === 'error') {\n errorStream.write(`${line}\\n`);\n } else {\n // Treat untagged stderr as WARN by default (common in libs)\n warnStream.write(`${line}\\n`);\n }\n } catch {\n /* ignore */\n }\n });\n child.stderr.on('data', onErrLine);\n }\n\n // Stash log path metadata on the child\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (child as any)[LOG_PATHS_SYM] = {\n structuredPath,\n outPath,\n errPath,\n infoPath,\n warnPath,\n errorPath,\n } as WorkerLogPaths;\n\n if (openLogWindows) {\n openLogTailWindowMulti(\n [structuredPath, outPath, errPath, infoPath, warnPath, errorPath],\n `worker-${id}`,\n isSilent,\n );\n }\n\n // Best-effort error suppression on file streams\n outStream.on('error', () => {\n /* ignore */\n });\n errStream.on('error', () => {\n /* ignore */\n });\n infoStream.on('error', () => {\n /* ignore */\n });\n warnStream.on('error', () => {\n /* ignore */\n });\n errorStream.on('error', () => {\n /* ignore */\n });\n\n return child;\n}\n","/* eslint-disable no-continue, no-control-regex */\nimport { readFileSync } from 'node:fs';\nimport type { WorkerLogPaths } from './spawnWorkerProcess';\n\n/**\n * Log locations\n */\nexport type LogLocation = 'out' | 'err' | 'structured' | 'warn' | 'info';\n\n/**\n * Which logs to show in the combined output.\n * Can include 'out' (stdout), 'err' (stderr), 'structured' (\n */\nexport type WhichLogs = Array<LogLocation>;\n\n/**\n * Show combined logs from all worker processes.\n *\n * @param slotLogPaths - Map of worker IDs to their log file paths.\n * @param whichList - one or more sources to include (e.g., ['err','out'])\n * @param filterLevel - 'error', 'warn', or 'all' to filter log levels.\n */\nexport function showCombinedLogs(\n slotLogPaths: Map<number, WorkerLogPaths | undefined>,\n whichList: WhichLogs,\n filterLevel: 'error' | 'warn' | 'all',\n): void {\n process.stdout.write('\\x1b[2J\\x1b[H');\n\n const isError = (t: string): boolean =>\n /\\b(ERROR|uncaughtException|unhandledRejection)\\b/i.test(t);\n const isWarnTag = (t: string): boolean => /\\b(WARN|WARNING)\\b/i.test(t);\n\n const lines: string[] = [];\n\n for (const [, paths] of slotLogPaths) {\n if (!paths) continue;\n\n const files: Array<{\n /** Absolute file path to read from */\n path: string;\n /** Source type for this file, used for classification */\n src: LogLocation;\n }> = [];\n for (const which of whichList) {\n if (which === 'out' && paths.outPath) {\n files.push({ path: paths.outPath, src: 'out' });\n }\n if (which === 'err' && paths.errPath) {\n files.push({ path: paths.errPath, src: 'err' });\n }\n if (which === 'structured' && paths.structuredPath) {\n files.push({ path: paths.structuredPath, src: 'structured' });\n }\n if (paths.warnPath && which === 'warn') {\n files.push({ path: paths.warnPath, src: 'warn' });\n }\n if (paths.infoPath && which === 'info') {\n files.push({ path: paths.infoPath, src: 'info' });\n }\n }\n\n for (const { path, src } of files) {\n let text = '';\n try {\n text = readFileSync(path, 'utf8');\n } catch {\n continue;\n }\n\n for (const ln of text.split('\\n')) {\n if (!ln) continue;\n\n const clean = ln.replace(/\\x1B\\[[0-9;]*m/g, '');\n\n if (filterLevel === 'all') {\n lines.push(ln);\n continue;\n }\n\n if (filterLevel === 'error') {\n if (isError(clean)) lines.push(ln);\n continue;\n }\n\n // filterLevel === 'warn'\n // Accept:\n // - explicit WARN tag anywhere\n // - OR lines from stderr that are NOT explicit errors (many warn libs print to stderr)\n // - OR lines containing the word \"warning\" (common in some libs)\n if (isWarnTag(clean) || (src === 'err' && !isError(clean))) {\n lines.push(ln);\n continue;\n }\n }\n }\n }\n\n // simple time-sort; each worker often prefixes ISO timestamps\n lines.sort((a, b) => {\n const ta = a.match(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/)?.[0] ?? '';\n const tb = b.match(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/)?.[0] ?? '';\n return ta.localeCompare(tb);\n });\n\n process.stdout.write(`${lines.join('\\n')}\\n`);\n process.stdout.write('\\nPress Esc/Ctrl+] to return to dashboard.\\n');\n}\n/* eslint-enable no-continue, no-control-regex */\n","import { createReadStream, statSync } from 'node:fs';\n\n/**\n * Replay the tail of a file to stdout.\n *\n * @param path - The absolute path to the file to read.\n * @param maxBytes - The maximum number of bytes to read from the end of the file.\n * @param write - A function to write the output to stdout.\n */\nexport async function replayFileTailToStdout(\n path: string,\n maxBytes: number,\n write: (s: string) => void,\n): Promise<void> {\n await new Promise((resolve) => {\n try {\n const st = statSync(path);\n const start = Math.max(0, st.size - maxBytes);\n const stream = createReadStream(path, { start, encoding: 'utf8' });\n stream.on('data', (chunk) => write(chunk as string));\n stream.on('end', resolve);\n stream.on('error', resolve);\n } catch {\n resolve(undefined);\n }\n });\n}\n","import type * as readline from 'node:readline';\n\n/**\n * Map a key press to an action in the interactive dashboard.\n */\nexport type Action =\n | {\n /** Indicates attaching to a session by id. */\n type: 'ATTACH';\n /** The id of the session to attach to. */\n id: number;\n }\n | {\n /** Indicates cycling through sessions. */\n type: 'CYCLE';\n /** The direction to cycle: +1 for next, -1 for previous. */\n delta: number;\n }\n | {\n /** Indicates detaching from the current session. */\n type: 'DETACH';\n }\n | {\n /** Indicates the Ctrl+C key combination was pressed. */\n type: 'CTRL_C';\n }\n | {\n /** Indicates the Ctrl+D key combination was pressed. */\n type: 'CTRL_D';\n }\n | {\n /** Indicates quitting the dashboard. */\n type: 'QUIT';\n }\n | {\n /** Forwards an unhandled key sequence. */\n type: 'FORWARD';\n /** The key sequence to forward. */\n sequence: string;\n };\n\n/**\n * Map a key press to an action in the interactive dashboard.\n *\n * @param str - The string representation of the key press.\n * @param key - The key object containing details about the key press.\n * @param mode - The current mode of the dashboard, either 'dashboard' or 'attached'.\n * @returns An Action object representing the mapped action, or null if no action is mapped.\n */\nexport function keymap(\n str: string,\n key: readline.Key,\n mode: 'dashboard' | 'attached',\n): Action | null {\n if (key.ctrl && key.name === 'c') return { type: 'CTRL_C' };\n\n if (mode === 'dashboard') {\n if (key.name && /^[0-9]$/.test(key.name)) {\n return { type: 'ATTACH', id: Number(key.name) };\n }\n if (key.name === 'tab' && !key.shift) return { type: 'CYCLE', delta: +1 };\n if (key.name === 'tab' && key.shift) return { type: 'CYCLE', delta: -1 };\n if (key.name === 'q') return { type: 'QUIT' };\n return null;\n }\n\n // attached\n if (key.name === 'escape' || (key.ctrl && key.name === ']')) {\n return { type: 'DETACH' };\n }\n if (key.ctrl && key.name === 'd') return { type: 'CTRL_D' };\n\n const sequence = key.sequence ?? str ?? '';\n return sequence ? { type: 'FORWARD', sequence } : null;\n}\n","import type { ChildProcess } from 'node:child_process';\n\n/**\n * Get the sorted list of worker IDs from a map of ChildProcess instances.\n *\n * @param m - Map of worker IDs to ChildProcess instances.\n * @returns Sorted array of worker IDs.\n */\nexport function getWorkerIds(m: Map<number, ChildProcess>): number[] {\n return [...m.keys()].sort((a, b) => a - b);\n}\n\n/**\n * Cycles through an array of numeric IDs, returning the next ID based on a delta.\n *\n * If the `current` ID is not provided or not found in the array, the first ID is used as the starting point.\n * The function then moves forward or backward in the array by `delta` positions, wrapping around if necessary.\n *\n * @param ids - Array of numeric IDs to cycle through.\n * @param current - The current ID to start cycling from. If `null` or not found, starts from the first ID.\n * @param delta - The number of positions to move forward (positive) or backward (negative) in the array.\n * @returns The next ID in the array after cycling, or `null` if the array is empty.\n */\nexport function cycleWorkers(\n ids: number[],\n current: number | null,\n delta: number,\n): number | null {\n if (!ids.length) return null;\n const cur = current == null ? ids[0] : current;\n let i = ids.indexOf(cur);\n if (i === -1) i = 0;\n i = (i + delta + ids.length) % ids.length;\n return ids[i]!;\n}\n","import * as readline from 'node:readline';\nimport type { ChildProcess } from 'node:child_process';\nimport type { WorkerLogPaths } from './spawnWorkerProcess';\nimport { replayFileTailToStdout } from './replayFileTailToStdout';\nimport { keymap } from './keymap';\nimport { cycleWorkers, getWorkerIds } from './workerIds';\nimport type { WhichLogs } from './showCombinedLogs';\nimport { DEBUG } from '../../constants';\n\n/**\n * Key action types for the interactive switcher\n */\nexport type InteractiveDashboardMode = 'dashboard' | 'attached';\n\nexport interface SwitcherPorts {\n /** Standard input stream */\n stdin: NodeJS.ReadStream;\n /** Standard output stream */\n stdout: NodeJS.WriteStream;\n /** Standard error stream */\n stderr: NodeJS.WriteStream;\n}\n\n/**\n * Install an interactive switcher for managing worker processes.\n *\n * @param opts - Options for the switcher\n * @returns A cleanup function to remove the switcher\n */\nexport function installInteractiveSwitcher(opts: {\n /** Registry of live workers by id */\n workers: Map<number, ChildProcess>;\n /** Hooks */\n onAttach?: (id: number) => void;\n /** Optional detach handler */\n onDetach?: () => void;\n /** Optional Ctrl+C handler for parent graceful shutdown in dashboard */\n onCtrlC?: () => void; // parent graceful shutdown in dashboard\n /** Provide log paths so we can replay the tail on attach */\n getLogPaths?: (id: number) => WorkerLogPaths | undefined;\n /** How many bytes to replay from the end of each file (default 200 KB) */\n replayBytes?: number;\n /** Which logs to replay first (default ['out','err']) */\n replayWhich?: WhichLogs;\n /** Print a small banner/clear screen before replaying (optional) */\n onEnterAttachScreen?: (id: number) => void;\n /** Optional stdio ports for testing; defaults to process stdio */\n ports?: SwitcherPorts;\n}): () => void {\n const {\n workers,\n onAttach,\n onDetach,\n onCtrlC,\n getLogPaths,\n replayBytes = 200 * 1024,\n replayWhich = ['out', 'err'],\n onEnterAttachScreen,\n ports,\n } = opts;\n\n const stdin = ports?.stdin ?? process.stdin;\n const stdout = ports?.stdout ?? process.stdout;\n const stderr = ports?.stderr ?? process.stderr;\n\n const d = (...a: unknown[]): void => {\n if (DEBUG) {\n try {\n (ports?.stderr ?? process.stderr).write(\n `[keys] ${a.map(String).join(' ')}\\n`,\n );\n } catch {\n // noop\n }\n }\n };\n\n if (!stdin.isTTY) {\n // Not a TTY; return a no-op cleanup\n return () => {\n // noop\n };\n }\n\n readline.emitKeypressEvents(stdin);\n stdin.setRawMode?.(true);\n\n let mode: InteractiveDashboardMode = 'dashboard';\n let focus: number | null = null;\n\n // live mirroring handlers while attached\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let outHandler: ((chunk: any) => void) | null = null;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let errHandler: ((chunk: any) => void) | null = null;\n\n /**\n * Cycle through worker IDs, wrapping around.\n *\n * @param id - The current worker ID to start cycling from.\n * @returns The next worker ID after cycling, or null if no workers are available.\n */\n async function replayLogs(id: number): Promise<void> {\n if (!getLogPaths) return;\n const paths = getLogPaths(id);\n if (!paths) return;\n\n const toReplay: string[] = [];\n for (const which of replayWhich) {\n if (which === 'out') toReplay.push(paths.outPath);\n if (which === 'err') toReplay.push(paths.errPath);\n if (which === 'structured') toReplay.push(paths.structuredPath);\n }\n\n if (toReplay.length) {\n stdout.write('\\n------------ replay ------------\\n');\n for (const p of toReplay) {\n stdout.write(\n `\\n--- ${p} (last ~${Math.floor(replayBytes / 1024)}KB) ---\\n`,\n );\n await replayFileTailToStdout(p, replayBytes, (s) => stdout.write(s));\n }\n stdout.write('\\n--------------------------------\\n\\n');\n }\n }\n\n const attach = async (id: number): Promise<void> => {\n d('attach()', `id=${id}`); // at function entry\n\n const w = workers.get(id);\n if (!w) return;\n\n // Detach any previous focus\n if (mode === 'attached') detach();\n\n mode = 'attached';\n focus = id;\n\n // UX: clear + banner\n onEnterAttachScreen?.(id);\n\n onAttach?.(id); // prints “Attached to worker …” and clears\n await replayLogs(id); // now the tail stays visible\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n outHandler = (chunk: any) => stdout.write(chunk);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n errHandler = (chunk: any) => stderr.write(chunk);\n w.stdout?.on('data', outHandler);\n w.stderr?.on('data', errHandler);\n\n // auto-detach if child exits\n const onExit = (): void => {\n if (focus === id) detach();\n };\n w.once('exit', onExit);\n };\n\n const detach = (): void => {\n d('detach()', `id=${focus}`); // at function entry\n\n if (focus == null) return;\n const id = focus;\n const w = workers.get(id);\n if (w) {\n if (outHandler) w.stdout?.off('data', outHandler);\n if (errHandler) w.stderr?.off('data', errHandler);\n }\n outHandler = null;\n errHandler = null;\n focus = null;\n mode = 'dashboard';\n onDetach?.();\n };\n\n const onKey = (str: string, key: readline.Key): void => {\n d(\n 'keypress',\n JSON.stringify({\n str,\n name: key.name,\n seq: key.sequence,\n ctrl: key.ctrl,\n meta: key.meta,\n shift: key.shift,\n mode,\n }),\n );\n const act = keymap(str, key, mode);\n d('mapped', JSON.stringify(act));\n\n if (!act) return;\n\n // eslint-disable-next-line default-case\n switch (act.type) {\n case 'CTRL_C': {\n d('CTRL_C');\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.kill('SIGINT');\n } catch {\n // noop\n }\n // optional: auto-detach so second Ctrl+C exits parent\n detach();\n return;\n }\n onCtrlC?.();\n return;\n }\n\n case 'ATTACH': {\n d('ATTACH', `id=${act.id}`, `has=${workers.has(act.id)}`);\n\n if (mode !== 'dashboard') return;\n // eslint-disable-next-line no-void\n if (workers.has(act.id)) void attach(act.id);\n return;\n }\n\n case 'CYCLE': {\n d('CYCLE', `delta=${act.delta}`);\n if (mode !== 'dashboard') return;\n const next = cycleWorkers(getWorkerIds(workers), focus, act.delta);\n // eslint-disable-next-line no-void\n if (next != null) void attach(next);\n return;\n }\n\n case 'QUIT': {\n if (mode !== 'dashboard') return;\n onCtrlC?.();\n return;\n }\n\n case 'DETACH': {\n d('DETACH');\n if (mode === 'attached') detach();\n return;\n }\n\n case 'CTRL_D': {\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.stdin?.end();\n } catch {\n // noop\n }\n }\n return;\n }\n\n case 'FORWARD': {\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.stdin?.write(act.sequence);\n } catch {\n // noop\n }\n }\n }\n }\n };\n\n // Raw bytes fallback (usually not hit because keypress handles it)\n const onData = (chunk: Buffer): void => {\n if (mode === 'attached' && focus != null) {\n const w = workers.get(focus);\n try {\n w?.stdin?.write(chunk);\n } catch {\n // noop\n }\n }\n };\n\n const cleanup = (): void => {\n stdin.off('keypress', onKey);\n stdin.off('data', onData);\n stdin.setRawMode?.(false);\n stdout.write('\\x1b[?25h');\n };\n\n stdin.on('keypress', onKey);\n stdin.on('data', onData);\n\n return cleanup;\n}\n","// lib/ui/dashboardPlugin.ts\nimport * as readline from 'node:readline';\nimport colors from 'colors';\nimport type { SlotState } from './types';\nimport type { ObjByString } from '@transcend-io/type-utils';\n\n/**\n * A dashboard plugin defines how to render the worker pool UI.\n * Commands can supply a plugin to customize:\n * - The header block (summary stats, title, etc.)\n * - Per-worker rows (one line per worker slot)\n * - Optional extras (artifact exports, breakdowns, footers)\n *\n * @template TTotals - The shape of the aggregate totals object maintained by the command.\n */\nexport interface DashboardPlugin<TTotals, TSlotState extends ObjByString> {\n /**\n * Render the header block of the dashboard.\n *\n * @param ctx - Context with pool/worker state, totals, and metadata.\n * @returns An array of strings, each representing one line in the header.\n */\n renderHeader: (ctx: CommonCtx<TTotals, TSlotState>) => string[];\n\n /**\n * Render per-worker rows, usually one line per worker slot.\n *\n * @param ctx - Context with pool/worker state, totals, and metadata.\n * @returns An array of strings, each representing one row in the workers section.\n */\n renderWorkers: (ctx: CommonCtx<TTotals, TSlotState>) => string[];\n\n /**\n * Render any optional extra blocks that appear after the worker rows.\n * Useful for printing export paths, aggregated metrics, breakdowns, etc.\n *\n * @param ctx - Context with pool/worker state, totals, and metadata.\n * @returns An array of strings, each representing one additional line.\n */\n renderExtras?: (ctx: CommonCtx<TTotals, TSlotState>) => string[];\n}\n\n/**\n * Shared context object passed into all render methods of a {@link DashboardPlugin}.\n *\n * @template TTotals - The shape of the aggregate totals object maintained by the command.\n */\nexport type CommonCtx<TTotals, TSlotState extends ObjByString> = {\n /** Human-readable title for the dashboard (e.g., \"Parallel uploader\"). */\n title: string;\n\n /** Number of worker processes spawned in the pool. */\n poolSize: number;\n\n /** Logical CPU count, included for informational display. */\n cpuCount: number;\n\n /** Total number of \"files\" or logical units the command expects to process. */\n filesTotal: number;\n\n /** Count of successfully completed files/tasks. */\n filesCompleted: number;\n\n /** Count of failed files/tasks. */\n filesFailed: number;\n\n /**\n * State of each worker slot, keyed by worker id.\n * Includes busy flag, file label, start time, last log badge, and progress.\n */\n workerState: Map<number, SlotState<TSlotState>>;\n\n /**\n * Aggregate totals maintained by the command’s hook logic.\n * Domain-specific metrics (e.g., rows uploaded, bytes processed) can be surfaced here.\n */\n totals: TTotals;\n\n /**\n * Throughput metrics tracked by the runner:\n * - successSoFar: convenience alias for completed count\n * - r10s: completions/sec averaged over last 10s\n * - r60s: completions/sec averaged over last 60s\n */\n throughput: {\n /** Cumulative count of successful completions so far. */\n successSoFar: number;\n /** Recent file-level throughput rate over the last 10 seconds. */\n r10s: number;\n /** Recent file-level throughput rate over the last 60 seconds. */\n r60s: number;\n /** Recent job/record-level throughput rate over the last 10 seconds. */\n jobsR10s: number;\n /** Recent job/record-level throughput rate over the last 60 seconds. */\n jobsR60s: number;\n };\n\n /** True when the pool has fully drained and all workers have exited. */\n final: boolean;\n\n /**\n * Optional export status payload provided by the command.\n * Useful for rendering artifact paths or \"latest export\" summaries.\n */\n exportStatus?: Record<string, unknown>;\n};\n\n/** The most recently rendered frame, cached to suppress flicker from duplicate renders. */\nlet lastFrame = '';\n\n/**\n * Generate the hotkeys hint string that appears at the bottom of the dashboard.\n *\n * @param poolSize - The number of worker slots in the pool.\n * @param final - Whether the run has completed.\n * @returns A dimmed string listing the supported hotkeys for attach/detach/quit.\n */\nexport const hotkeysHint = (poolSize: number, final: boolean): string => {\n const maxDigit = Math.min(poolSize - 1, 9);\n const digitRange = poolSize <= 1 ? '0' : `0-${maxDigit}`;\n const extra = poolSize > 10 ? ' (Tab/Shift+Tab for ≥10)' : '';\n return final\n ? colors.dim(\n 'Run complete — digits to view logs • Tab/Shift+Tab cycle • Esc/Ctrl+] detach • q to quit',\n )\n : colors.dim(\n `Hotkeys: [${digitRange}] attach${extra} • e=errors • w=warnings • i=info • l=logs • ` +\n 'Tab/Shift+Tab • Esc/Ctrl+] detach • Ctrl+C exit',\n );\n};\n\n/**\n * Render the dashboard using a supplied {@link DashboardPlugin}.\n *\n * The frame is composed of:\n * - Header lines\n * - A blank separator\n * - Worker rows\n * - A blank separator\n * - Hotkeys hint\n * - Optional extras (if plugin supplies them)\n *\n * Optimizations:\n * - Suppresses re-renders if the frame is identical to the previous frame (flicker-free).\n * - Hides the terminal cursor during live updates, restoring it when final.\n *\n * @param ctx - Shared context containing pool state, worker state, totals, throughput, etc.\n * @param plugin - The plugin that defines how to render the header, workers, and optional extras.\n * @param viewerMode - If true, renders in viewer mode (no ability to switch between files).\n */\nexport function dashboardPlugin<TTotals, TSlotState extends ObjByString>(\n ctx: CommonCtx<TTotals, TSlotState>,\n plugin: DashboardPlugin<TTotals, TSlotState>,\n viewerMode = false,\n): void {\n const frame = [\n ...plugin.renderHeader(ctx),\n '',\n ...plugin.renderWorkers(ctx),\n ...(viewerMode ? [] : ['', hotkeysHint(ctx.poolSize, ctx.final)]),\n ...(plugin.renderExtras ? [''].concat(plugin.renderExtras(ctx)) : []),\n ].join('\\n');\n\n // Skip duplicate renders during live runs to avoid flicker.\n if (!ctx.final && frame === lastFrame) return;\n lastFrame = frame;\n\n if (!ctx.final) {\n // Hide cursor and repaint in place\n process.stdout.write('\\x1b[?25l');\n readline.cursorTo(process.stdout, 0, 0);\n readline.clearScreenDown(process.stdout);\n } else {\n // Restore cursor on final render\n process.stdout.write('\\x1b[?25h');\n }\n process.stdout.write(`${frame}\\n`);\n}\n","import type { ChildProcess } from 'node:child_process';\nimport {\n getWorkerLogPaths,\n isIpcOpen,\n type WorkerLogPaths,\n} from './spawnWorkerProcess';\n\n/**\n * Safely retrieve log paths for a worker slot.\n *\n * @param id - The worker slot ID\n * @param workers - Map of worker IDs to their ChildProcess instances\n * @param slotLogPaths - Map of worker IDs to their log paths\n * @returns The log paths for the worker slot, or undefined if not available\n */\nexport function safeGetLogPathsForSlot(\n id: number,\n workers: Map<number, ChildProcess>,\n slotLogPaths: Map<number, WorkerLogPaths | undefined>,\n): WorkerLogPaths | undefined {\n const live = workers.get(id);\n if (isIpcOpen(live)) {\n try {\n const p = getWorkerLogPaths(live!);\n if (p !== undefined && p !== null) return p;\n } catch {\n /* fall back */\n }\n }\n return slotLogPaths.get(id);\n}\n","import colors from 'colors';\nimport { basename } from 'node:path';\nimport type { CommonCtx } from './dashboardPlugin';\nimport type { ObjByString } from '@transcend-io/type-utils';\n\n/**\n * Progress snapshot for a worker slot in the chunk-csv command.\n */\nexport type ChunkSlotProgress = {\n /** Absolute path of the file being processed by this worker. */\n filePath?: string;\n /** Number of rows processed so far in this file. */\n processed?: number;\n /** Optional total number of rows in the file (if known). */\n total?: number;\n};\n\n/**\n * Format a number safely for display.\n *\n * @param n - The number to format (or `undefined`).\n * @returns A localized string representation, or \"0\".\n */\nexport function fmtNum(n: number | undefined): string {\n return typeof n === 'number' ? n.toLocaleString() : '0';\n}\n\n/**\n * Draw a horizontal bar of length `width` filled to `pct` percent.\n *\n * @param pct - Percentage 0..100.\n * @param width - Number of characters in the bar.\n * @returns A string like \"████░░░░\".\n */\nexport function pctBar(pct: number, width = 40): string {\n const clamped = Math.max(0, Math.min(100, Math.floor(pct)));\n const filled = Math.floor((clamped / 100) * width);\n return '█'.repeat(filled) + '░'.repeat(width - filled);\n}\n\n/**\n * Compute pool-wide progress values needed by headers.\n *\n * @param ctx - Dashboard context containing pool state, worker state, totals, etc.\n * @returns An object with `done`, `inProgress`, and `pct` properties.\n */\nexport function poolProgress<TTotals, TSlot extends ObjByString>(\n ctx: CommonCtx<TTotals, TSlot>,\n): {\n /** Count of successfully completed files/tasks. */\n done: number;\n /** Count of currently in-progress files/tasks. */\n inProgress: number;\n /** Percentage of completion (0-100). */\n pct: number;\n} {\n const inProgress = [...ctx.workerState.values()].filter((s) => s.busy).length;\n const done = ctx.filesCompleted + ctx.filesFailed;\n const pct =\n ctx.filesTotal === 0\n ? 100\n : Math.floor((done / Math.max(1, ctx.filesTotal)) * 100);\n return { done, inProgress, pct };\n}\n\n/**\n * Compose the common header lines (title, pool stats, progress bar, throughput).\n *\n * @param ctx - Dashboard context.\n * @param extraLines - Optional extra lines (e.g., totals block).\n * @returns Header lines.\n */\nexport function makeHeader<TTotals, TSlot extends ObjByString>(\n ctx: CommonCtx<TTotals, TSlot>,\n extraLines: string[] = [],\n): string[] {\n const {\n title,\n poolSize,\n cpuCount,\n filesTotal,\n filesCompleted,\n filesFailed,\n throughput,\n } = ctx;\n const { inProgress, pct } = poolProgress(ctx);\n\n const lines: string[] = [\n `${colors.bold(title)} — ${poolSize} workers ${colors.dim(\n `(CPU avail: ${cpuCount})`,\n )}`,\n `${colors.dim('Files')} ${fmtNum(filesTotal)} ${colors.dim(\n 'Completed',\n )} ${fmtNum(filesCompleted)} ${colors.dim('Failed')} ${\n filesFailed ? colors.red(fmtNum(filesFailed)) : fmtNum(filesFailed)\n } ${colors.dim('In-flight')} ${fmtNum(inProgress)}`,\n `[${pctBar(pct)}] ${pct}%`,\n ];\n\n if (throughput) {\n const jobsActive = throughput.jobsR10s > 0 || throughput.jobsR60s > 0;\n const perHour10 = Math.round(\n (jobsActive ? throughput.jobsR10s : throughput.r10s) * 3600,\n ).toLocaleString();\n const perHour60 = Math.round(\n (jobsActive ? throughput.jobsR60s : throughput.r60s) * 3600,\n ).toLocaleString();\n const unit = jobsActive ? 'rec' : 'files';\n const suffix =\n ctx.throughput?.successSoFar != null\n ? ` Newly uploaded: ${fmtNum(ctx.throughput.successSoFar)}`\n : '';\n lines.push(\n colors.cyan(\n `Throughput: ${perHour10} ${unit}/hr (1h: ${perHour60} ${unit}/hr)${suffix}`,\n ),\n );\n }\n\n return extraLines.length ? lines.concat(extraLines) : lines;\n}\n\n/**\n * Render per-worker rows with a compact progress bar and status badge.\n *\n * @param ctx - Dashboard context (slot progress type must have processed/total?).\n * @param getFileLabel - Optional: override how the filename is shown.\n * @returns Array of strings, each representing one worker row.\n */\nexport function makeWorkerRows<\n TTotals,\n TSlot extends Omit<ChunkSlotProgress, 'filePath'>,\n>(\n ctx: CommonCtx<TTotals, TSlot>,\n getFileLabel: (file: string | null | undefined) => string = (file) =>\n file ? basename(file) : '-',\n): string[] {\n const miniWidth = 18;\n\n return [...ctx.workerState.entries()].map(([id, s]) => {\n const badge =\n s.lastLevel === 'error'\n ? colors.red('ERROR ')\n : s.lastLevel === 'warn'\n ? colors.yellow('WARN ')\n : s.busy\n ? colors.green('WORKING')\n : colors.dim('IDLE ');\n\n const fname = getFileLabel(s.file);\n const elapsed = s.startedAt\n ? `${Math.floor((Date.now() - s.startedAt) / 1000)}s`\n : '-';\n\n const processed = s.progress?.processed ?? 0;\n const total = s.progress?.total ?? 0;\n const pctw = total > 0 ? Math.floor((processed / total) * 100) : 0;\n const mini = total > 0 ? pctBar(pctw, miniWidth) : ' '.repeat(miniWidth);\n const miniTxt =\n total > 0\n ? `${processed.toLocaleString()}/${total.toLocaleString()} (${pctw}%)`\n : colors.dim('—');\n\n return ` [w${id}] ${badge} | ${fname} | ${elapsed} | [${mini}] ${miniTxt}`;\n });\n}\n","/* eslint-disable max-lines */\nimport colors from 'colors';\nimport type { ChildProcess } from 'node:child_process';\nimport { RateCounter } from '../helpers';\nimport type { SlotState, FromWorker, ToWorker } from './types';\nimport {\n getWorkerLogPaths,\n isIpcOpen,\n safeSend,\n spawnWorkerProcess,\n type WorkerLogPaths,\n} from './spawnWorkerProcess';\nimport { classifyLogLevel, initLogDir, makeLineSplitter } from './logRotation';\nimport { safeGetLogPathsForSlot } from './safeGetLogPathsForSlot';\nimport { installInteractiveSwitcher } from './installInteractiveSwitcher';\nimport type { ObjByString } from '@transcend-io/type-utils';\n\n/**\n * Callbacks used by the generic pool orchestrator to:\n * - fetch tasks,\n * - format labels for UI,\n * - fold progress and results into aggregate totals,\n * - run optional post-processing once the pool completes.\n *\n * Each command supplies concrete `TTask`, `TProg`, `TRes`, and optionally a\n * custom totals type `TTotals`.\n */\nexport interface PoolHooks<\n TTask extends ObjByString,\n TProg extends ObjByString,\n TRes extends ObjByString,\n TTotals = unknown,\n> {\n /**\n * Produce the next work item for a slot.\n *\n * @returns The next task or `undefined` if no tasks remain.\n */\n nextTask: () => TTask | undefined;\n\n /**\n * Human-readable label for a task, shown in dashboards.\n *\n * @param t - The task to label.\n * @returns A short descriptor, typically a file path or identifier.\n */\n taskLabel: (t: TTask) => string;\n\n /**\n * Fold an incoming progress payload into aggregate totals.\n * Should be pure (no side effects) and return the new totals object.\n *\n * @param prevTotals - The previous totals value.\n * @param prog - The latest progress payload from a worker.\n * @returns Updated totals.\n */\n onProgress: (prevTotals: TTotals, prog: TProg) => TTotals;\n\n /**\n * Handle a final result from a worker.\n * Should be pure and return the new totals plus a boolean indicating if the\n * unit succeeded (used to set per-slot level/metrics).\n *\n * @param prevTotals - The previous totals value.\n * @param res - The result payload from a worker.\n * @returns Object containing updated totals and success flag.\n */\n onResult: (\n prevTotals: TTotals,\n res: TRes,\n ) => {\n /** Updated totals after processing this result */\n totals: TTotals;\n /** Whether the task was successful */\n ok: boolean;\n };\n\n /**\n * Initialize per-slot progress state when a task is assigned.\n * Useful when you want a non-undefined `progress` immediately.\n *\n * @param t - The task to be started in this slot.\n * @returns Initial progress state or `undefined`.\n */\n initSlotProgress?: (t: TTask) => TProg | undefined;\n\n /**\n * Produce the initial totals value for the pool (defaults to `{}`).\n *\n * @returns A new totals object.\n */\n initTotals?: () => TTotals;\n\n /**\n * Provide an export status map for dashboards (optional).\n *\n * @returns A status object or `undefined` if not applicable.\n */\n exportStatus?: () => Record<string, unknown> | undefined;\n\n /**\n * Optional post-processing step invoked after the pool finishes.\n * Common use: writing combined logs/artifacts once all workers complete.\n *\n * When {@link RunPoolOptions.viewerMode} is enabled, the runner also passes\n * the **log directory** and the **per-slot log file paths** so you can\n * replicate the legacy “viewer mode” auto-exports (combined logs, indices, etc.).\n */\n postProcess?: (ctx: {\n /** Live snapshot of all worker slots at completion. */\n slots: Map<number, SlotState<TProg>>;\n /** Final aggregate totals. */\n totals: TTotals;\n /** Absolute path to the pool’s log directory. */\n logDir: string;\n /**\n * Mapping of slot id -> log paths (stdout/stderr/current, rotations may exist).\n * Use this to collect and export artifacts after completion.\n */\n logsBySlot: Map<number, WorkerLogPaths | undefined>;\n /** Unix millis when the pool started (first worker spawned). */\n startedAt: number;\n /** Unix millis when the pool fully completed (after last worker exit). */\n finishedAt: number;\n /**\n * Helper to safely re-fetch a slot’s current log paths, accounting for respawns.\n * Mirrors the dashboard’s attach/switcher behavior.\n */\n getLogPathsForSlot: (id: number) => WorkerLogPaths | undefined;\n /** True if the pool was run in viewerMode (non-interactive). */\n viewerMode: boolean;\n }) => Promise<void> | void;\n}\n\n/**\n * Options to run a generic worker pool.\n *\n * @template TTask - The payload sent to each worker as a \"task\".\n * @template TProg - The progress payload emitted by workers.\n * @template TRes - The result payload emitted by workers.\n * @template TTotals - The aggregate totals object maintained by hooks.\n */\nexport interface RunPoolOptions<\n TTask extends ObjByString,\n TProg extends ObjByString,\n TRes extends ObjByString,\n TTotals extends ObjByString,\n> {\n /** Human-readable name for the pool, shown in headers (e.g., \"Parallel uploader\", \"Chunk CSV\"). */\n title: string;\n\n /**\n * Directory for pool-local state (logs, discovery messages, artifacts).\n * Usually the CLI's working directory for the command.\n */\n baseDir: string;\n\n /** Absolute path of the module the child should execute (the command impl that calls runChild when CHILD_FLAG is present). */\n childModulePath: string;\n\n /**\n * Number of worker processes to spawn. Typically derived via a helper like `computePoolSize`.\n */\n poolSize: number;\n\n /** Logical CPU count used for display only (not required to equal `poolSize`). */\n cpuCount: number;\n\n /**\n * Flag that the child module expects to see in `process.argv` to run in \"worker\" mode.\n * This MUST match the flag the worker module checks (e.g., `--as-child`).\n */\n childFlag: string;\n\n /**\n * Renderer function injected by the command. The runner calls this on each \"tick\"\n * and on significant state changes (progress, completion, attach/detach).\n */\n render: (input: {\n /** Header/title for the UI. */\n title: string;\n /** Configured pool size (number of workers). */\n poolSize: number;\n /** CPU count for informational display. */\n cpuCount: number;\n /** Total number of files/tasks anticipated by the command. */\n filesTotal: number;\n /** Number of files/tasks that have produced a successful result so far. */\n filesCompleted: number;\n /** Number of files/tasks that have produced a failed result so far. */\n filesFailed: number;\n /**\n * Per-slot state for each worker, including busy flag, file label, start time,\n * last log level badge, and optional progress payload.\n */\n workerState: Map<number, SlotState<TProg>>;\n /**\n * Arbitrary totals object maintained by hooks. This is the primary place to surface\n * domain-specific aggregate metrics in the UI.\n */\n totals: TTotals;\n /**\n * Smoothed throughput metrics computed by the runner:\n * - successSoFar: convenience mirror of completed count for the renderer\n * - r10s: moving average of completions per second over ~10 seconds\n * - r60s: moving average of completions per second over ~60 seconds\n */\n throughput: {\n /** Convenience mirror of `filesCompleted` for renderers that expect it in this block. */\n successSoFar: number;\n /** Moving average file completions/sec (10s window). */\n r10s: number;\n /** Moving average file completions/sec (60s window). */\n r60s: number;\n /** Moving average job/record completions/sec (10s window). */\n jobsR10s: number;\n /** Moving average job/record completions/sec (60s window). */\n jobsR60s: number;\n };\n /** True when the pool has fully drained and all workers have exited. */\n final: boolean;\n /**\n * Optional export status payload surfaced by hooks; used by commands that generate\n * multiple artifact files and want to show \"latest paths\" in the UI.\n */\n exportStatus?: Record<string, unknown>;\n }) => void;\n\n /**\n * Hook suite that adapts the pool to a specific command:\n * - nextTask(): TTask | undefined\n * - taskLabel(task): string\n * - initTotals?(): TTotals\n * - initSlotProgress?(task): TProg\n * - onProgress(totals, prog): TTotals\n * - onResult(totals, res): { totals: TTotals; ok: boolean }\n * - postProcess?({ slots, totals, logDir, logsBySlot, ... }): Promise<void> | void\n * - exportStatus?(): Record<string, unknown>\n */\n hooks: PoolHooks<TTask, TProg, TRes, TTotals>;\n\n /**\n * Total number of \"files\" or logical items the command expects to process.\n * Used purely for UI/ETA; does not affect scheduling.\n */\n filesTotal: number;\n\n /** Open worker logs in new terminals (macOS). Default true unless viewerMode=true. */\n openLogWindows?: boolean;\n\n /** Silence worker stdio (except logs). */\n isSilent?: boolean;\n\n /**\n * When true, run in “viewer mode” (non-interactive):\n * - Do NOT install the interactive attach/switcher.\n * - Default `openLogWindows` to false.\n * - Still render on a timer.\n * - Provide `logDir`/`logsBySlot` to `postProcess` for auto-exports.\n */\n viewerMode?: boolean;\n\n /**\n * Optional factory for additional key bindings (e.g., log viewers/exports).\n * Only used when viewerMode === false.\n */\n extraKeyHandler?: (args: {\n /** per-slot log paths (kept up-to-date across respawns) */\n logsBySlot: Map<number, WorkerLogPaths | undefined>;\n /** re-render dashboard now */\n repaint: () => void;\n /** pause/unpause dashboard repaint while showing viewers */\n setPaused: (p: boolean) => void;\n }) => (buf: Buffer) => void;\n}\n\n/**\n * Run a multi-process worker pool for a command.\n * The runner owns: spawning workers, assigning tasks, collecting progress/results,\n * basic log badging (WARN/ERROR), an interactive attach/switcher (unless viewerMode),\n * and a render loop.\n *\n * The command injects \"hooks\" to customize scheduling and totals aggregation.\n *\n * @param opts - Options\n */\nexport async function runPool<\n TTask extends ObjByString,\n TProg extends ObjByString,\n TRes extends ObjByString,\n TTotals extends ObjByString,\n>(opts: RunPoolOptions<TTask, TProg, TRes, TTotals>): Promise<void> {\n const {\n title,\n baseDir,\n poolSize,\n cpuCount,\n render,\n childModulePath,\n hooks,\n filesTotal,\n childFlag,\n viewerMode = false,\n } = opts;\n\n // Default behaviors may change under viewerMode.\n const openLogWindows = opts.openLogWindows ?? !viewerMode;\n const isSilent = opts.isSilent ?? true;\n\n const startedAt = Date.now();\n const logDir = initLogDir(baseDir);\n\n /** Live worker processes keyed by slot id. */\n const workers = new Map<number, ChildProcess>();\n /** Per-slot state tracked for the UI and scheduling. */\n const workerState = new Map<number, SlotState<TProg>>();\n /** File paths for each worker’s stdout/stderr logs. */\n const slotLogs = new Map<number, WorkerLogPaths | undefined>();\n /** File-completion throughput meter. */\n const meter = new RateCounter();\n /** Job/record-level throughput meter (fed from progress.processed deltas). */\n const jobMeter = new RateCounter();\n /** Last-seen `processed` count per worker slot, used to compute deltas. */\n const lastProcessed = new Map<number, number>();\n const totalsInit = (hooks.initTotals?.() ?? {}) as TTotals;\n\n let totalsBox = totalsInit;\n let activeWorkers = 0;\n let completed = 0;\n let failed = 0;\n\n // Repaint ticker starts on first READY to avoid double-first-render.\n let ticker: NodeJS.Timeout | null = null;\n let firstReady = false;\n // Gate repaint during popup viewers/exports (driven by extraKeyHandler).\n let paused = false;\n // Keep a reference so we can unbind on exit.\n let extraHandler: ((buf: Buffer) => void) | null = null;\n\n /**\n * Paint the UI. The renderer is intentionally pure and receives\n * a snapshot of current state.\n *\n * @param final - If true, render the final state and exit.\n */\n const repaint = (final = false): void => {\n if (paused) return;\n render({\n title,\n poolSize,\n cpuCount,\n filesTotal,\n filesCompleted: completed,\n filesFailed: failed,\n workerState,\n totals: totalsBox,\n final,\n exportStatus: hooks.exportStatus?.(),\n throughput: {\n successSoFar: completed,\n r10s: meter.rate(10_000),\n r60s: meter.rate(60_000),\n jobsR10s: jobMeter.rate(10_000),\n jobsR60s: jobMeter.rate(60_000),\n },\n });\n };\n\n /**\n * Assign the next task to `id` if available.\n *\n * @param id - The worker slot id to assign a task to.\n * @returns true if a task was assigned.\n *\n * NOTE: This is the critical fix. We **do not** \"peek & put back\" a task.\n * We only consume via `nextTask()` inside this function.\n */\n const assign = (id: number): boolean => {\n const task = hooks.nextTask();\n if (!task) return false;\n\n const child = workers.get(id)!;\n const label = hooks.taskLabel(task);\n const initialProg = hooks.initSlotProgress?.(task);\n\n workerState.set(id, {\n busy: true,\n file: label,\n startedAt: Date.now(),\n lastLevel: 'ok',\n progress: initialProg,\n });\n\n safeSend(child, { type: 'task', payload: task } as ToWorker<TTask>);\n repaint();\n return true;\n };\n\n /* Spawn workers */\n for (let i = 0; i < poolSize; i += 1) {\n const child = spawnWorkerProcess({\n id: i,\n modulePath: childModulePath,\n logDir,\n openLogWindows,\n isSilent,\n childFlag,\n });\n workers.set(i, child);\n workerState.set(i, {\n busy: false,\n file: null,\n startedAt: null,\n lastLevel: 'ok',\n });\n slotLogs.set(i, getWorkerLogPaths(child));\n activeWorkers += 1;\n\n // badge WARN/ERROR quickly from stderr\n const errLine = makeLineSplitter((line) => {\n const lvl = classifyLogLevel(line);\n if (!lvl) return;\n const prev = workerState.get(i)!;\n if (prev.lastLevel !== lvl) {\n workerState.set(i, { ...prev, lastLevel: lvl });\n repaint();\n }\n });\n child.stderr?.on('data', errLine);\n\n // messages from the worker\n // eslint-disable-next-line no-loop-func\n child.on('message', (msg: FromWorker<TProg, TRes>) => {\n if (!msg || typeof msg !== 'object') return;\n\n if (msg.type === 'ready') {\n if (!firstReady) {\n firstReady = true;\n ticker = setInterval(() => repaint(false), 350);\n }\n assign(i); // try to start work immediately\n return;\n }\n\n if (msg.type === 'progress') {\n totalsBox = hooks.onProgress(totalsBox, msg.payload);\n const prev = workerState.get(i)!;\n workerState.set(i, { ...prev, progress: msg.payload });\n\n // Feed job-level meter from progress.processed deltas\n const payload = msg.payload as Record<string, unknown>;\n if (typeof payload?.processed === 'number') {\n const prevCount = lastProcessed.get(i) ?? 0;\n const delta = payload.processed - prevCount;\n if (delta > 0) jobMeter.add(delta);\n lastProcessed.set(i, payload.processed);\n }\n\n repaint();\n return;\n }\n\n if (msg.type === 'result') {\n const prev = workerState.get(i)!;\n const { totals: t2, ok } = hooks.onResult(totalsBox, msg.payload);\n totalsBox = t2;\n\n if (ok) {\n completed += 1;\n meter.add(1);\n } else {\n failed += 1;\n }\n\n workerState.set(i, {\n ...prev,\n busy: false,\n file: null,\n progress: undefined,\n lastLevel: ok ? 'ok' : 'error',\n });\n lastProcessed.delete(i);\n\n // Just try to assign; if none left, shut this child down.\n if (!assign(i) && isIpcOpen(child)) {\n safeSend(child, { type: 'shutdown' } as ToWorker<TTask>);\n }\n repaint();\n }\n });\n\n // eslint-disable-next-line no-loop-func\n child.on('exit', () => {\n activeWorkers -= 1;\n if (activeWorkers === 0) {\n if (ticker) clearInterval(ticker);\n repaint(true);\n }\n });\n }\n\n /* Interactive attach/switcher */\n let cleanupSwitcher: () => void = () => {\n /* noop */\n // no-op by default, overridden in non-viewerMode\n };\n\n const tearDownStdin = (): void => {\n try {\n process.stdin.setRawMode?.(false);\n } catch {\n /* noop */\n }\n try {\n process.stdin.pause();\n } catch {\n /* noop */\n }\n };\n\n const onSigint = (): void => {\n if (ticker) clearInterval(ticker);\n cleanupSwitcher?.();\n if (extraHandler) {\n try {\n process.stdin.off('data', extraHandler);\n } catch {\n /* noop */\n }\n }\n tearDownStdin();\n\n process.stdout.write('\\nStopping workers...\\n');\n for (const [, w] of workers) {\n if (isIpcOpen(w)) safeSend(w, { type: 'shutdown' } as ToWorker<TTask>);\n try {\n w?.kill('SIGTERM');\n } catch {\n /* noop */\n }\n }\n process.exit(130);\n };\n\n const onAttach = (id: number): void => {\n paused = true; // stop dashboard repaint while attached/viewing\n process.stdout.write('\\x1b[2J\\x1b[H'); // clear + home\n process.stdout.write(\n `Attached to worker ${id}. (Esc/Ctrl+] detach • Ctrl+D EOF • Ctrl+C SIGINT)\\n`,\n );\n };\n const onDetach = (): void => {\n paused = false;\n repaint();\n };\n\n process.once('SIGINT', onSigint);\n\n if (!viewerMode) {\n if (process.stdin.isTTY) {\n try {\n process.stdin.setRawMode(true);\n } catch {\n process.stdout.write(\n colors.yellow(\n 'Warning: Unable to enable raw mode for interactive key handling.\\n',\n ),\n );\n }\n process.stdin.resume(); // keep stdin flowing (no encoding — raw Buffer)\n }\n\n cleanupSwitcher = installInteractiveSwitcher({\n workers,\n onAttach,\n onDetach,\n onCtrlC: onSigint,\n getLogPaths: (id) => safeGetLogPathsForSlot(id, workers, slotLogs),\n replayBytes: 200 * 1024,\n replayWhich: ['out', 'err'],\n onEnterAttachScreen: onAttach,\n });\n\n if (opts.extraKeyHandler) {\n extraHandler = opts.extraKeyHandler({\n logsBySlot: slotLogs,\n repaint: () => repaint(),\n setPaused: (p) => {\n paused = p;\n },\n });\n process.stdin.on('data', extraHandler);\n }\n }\n\n /* Wait for full completion, then post-process (with log context if needed). */\n await new Promise<void>((resolve) => {\n const check = setInterval(async () => {\n if (activeWorkers === 0) {\n clearInterval(check);\n if (ticker) clearInterval(ticker);\n cleanupSwitcher();\n\n if (extraHandler) {\n try {\n process.stdin.off('data', extraHandler);\n } catch {\n /* noop */\n }\n }\n tearDownStdin();\n\n const finishedAt = Date.now();\n\n try {\n await hooks.postProcess?.({\n slots: workerState,\n totals: totalsBox,\n logDir,\n logsBySlot: slotLogs,\n startedAt,\n finishedAt,\n viewerMode,\n getLogPathsForSlot: (id: number) =>\n safeGetLogPathsForSlot(id, workers, slotLogs),\n });\n } catch (err: unknown) {\n const msg =\n (\n err as {\n /** Error stack */\n stack?: string;\n }\n )?.stack ?? String(err);\n process.stdout.write(colors.red(`postProcess error: ${msg}\\n`));\n }\n resolve();\n }\n }, 300);\n });\n}\n/* eslint-enable max-lines */\n","import type { ExportStatusMap } from './logRotation';\nimport { showCombinedLogs, type LogLocation } from './showCombinedLogs';\nimport type { SlotPaths } from './spawnWorkerProcess';\n\n/** Severity filter applied by the viewer. */\ntype ViewLevel = 'error' | 'warn' | 'all';\n\n/**\n * Options for {@link createExtraKeyHandler}.\n */\nexport type CreateExtraKeyHandlerOpts = {\n /**\n * Per-slot log file paths maintained by the runner; used to stream or export logs.\n */\n logsBySlot: SlotPaths;\n\n /**\n * Request an immediate dashboard repaint (e.g., after updating export status).\n */\n repaint: () => void;\n\n /**\n * Pause/unpause dashboard repainting. The handler pauses while a viewer is open\n * to prevent the dashboard from overwriting the viewer output, then resumes on exit.\n */\n setPaused: (p: boolean) => void;\n\n /**\n * Optional export manager to enable uppercase export keys:\n * - `E` (errors) • `W` (warnings) • `I` (info) • `A` (all)\n *\n * Provide this only if your command supports writing combined log files.\n */\n exportMgr?: {\n /** Destination directory for exported artifacts. */\n exportsDir: string;\n /**\n * Write a combined log file for the selected severity and return the absolute path.\n *\n * @param logs - Log paths to combine.\n * @param which - Severity selection.\n * @returns Absolute path to the written file.\n */\n exportCombinedLogs: (\n logs: SlotPaths,\n which: 'error' | 'warn' | 'info' | 'all',\n ) => string;\n };\n\n /**\n * Optional “Exports” status map. If provided, the handler updates timestamps\n * when exports are written so your dashboard panel can reflect “last saved” times.\n */\n exportStatus?: ExportStatusMap;\n\n /**\n * Optional custom key bindings for command-specific actions.\n * Each handler receives helpers to print messages and to update the exports panel.\n *\n * Example:\n * ```ts\n * custom: {\n * F: async ({ say, noteExport }) => {\n * const p = await writeFailingUpdatesCsv(...);\n * say(`Wrote failing updates to: ${p}`);\n * noteExport('failuresCsv', p);\n * }\n * }\n * ```\n */\n custom?: Record<\n string,\n (ctx: {\n /** Update {@link exportStatus} (if present) and repaint the dashboard. */\n noteExport: (slot: keyof ExportStatusMap, absPath: string) => void;\n /** Print a line to stdout, automatically newline-terminated. */\n say: (s: string) => void;\n }) => void | Promise<void>\n >;\n};\n\n/**\n * Create a keypress handler for interactive viewers/exports.\n * Shared handler for \"extra\" keyboard shortcuts used by the interactive dashboard.\n *\n * It wires:\n * - **Viewers (lowercase):** `e` (errors), `w` (warnings), `i` (info), `l` (all)\n * - **Exports (uppercase, optional):** `E` (errors), `W` (warnings), `I` (info), `A` (all)\n * - **Dismiss:** `Esc` or `Ctrl+]` exits a viewer and returns to the dashboard\n * - **Custom keys (optional):** Provide a `custom` map to handle command-specific bindings\n *\n * Usage (inside `runPool({... extraKeyHandler })`):\n * ```ts\n * extraKeyHandler: ({ logsBySlot, repaint, setPaused }) =>\n * createExtraKeyHandler({ logsBySlot, repaint, setPaused })\n * ```\n *\n * If you also want export hotkeys + an \"Exports\" panel:\n * ```ts\n * extraKeyHandler: ({ logsBySlot, repaint, setPaused }) =>\n * createExtraKeyHandler({\n * logsBySlot, repaint, setPaused,\n * exportMgr, // enables E/W/I/A\n * exportStatus, // keeps panel timestamps up to date\n * custom: { // optional, e.g. 'F' to export a CSV\n * F: async ({ say, noteExport }) => { ... }\n * }\n * })\n * ```\n *\n * @param opts - Configuration for viewers, exports, and custom keys.\n * @returns A `(buf: Buffer) => void` handler suitable for `process.stdin.on('data', ...)`.\n */\nexport function createExtraKeyHandler(\n opts: CreateExtraKeyHandlerOpts,\n): (buf: Buffer) => void {\n const { logsBySlot, repaint, setPaused, exportMgr, exportStatus, custom } =\n opts;\n\n const say = (s: string): void => {\n process.stdout.write(`${s}\\n`);\n };\n\n /**\n * Record that an export was written and trigger a repaint so the dashboard’s\n * \"Exports\" panel shows the updated timestamp/path.\n *\n * @param slot - Slot name in {@link ExportStatusMap} (e.g., \"error\", \"warn\", etc.).\n * @param p - Absolute path to the exported file.\n */\n const noteExport = (slot: keyof ExportStatusMap, p: string): void => {\n const now = Date.now();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const cur: any = exportStatus?.[slot] ?? { path: p };\n if (exportStatus) {\n exportStatus[slot] = {\n path: p || cur.path,\n savedAt: now,\n exported: true,\n };\n repaint();\n }\n };\n\n let viewing = false; // optional guard to prevent stacking viewers\n\n /**\n * Show an inline combined log viewer for the selected sources/level.\n * Pauses dashboard repaint to keep the viewer visible until the user exits.\n *\n * @param sources - Log sources to include (e.g., \"err\", \"warn\", \"info\").\n * @param level - Severity level to filter by (e.g., \"error\", \"warn\", \"all\").\n */\n const view = (sources: LogLocation[], level: ViewLevel): void => {\n if (viewing) return;\n viewing = true;\n setPaused(true);\n\n // optional UX: clear screen and show a hint\n process.stdout.write('\\x1b[2J\\x1b[H'); // clear+home\n process.stdout.write(\n 'Combined logs viewer (press Esc or Ctrl+] to return)\\n\\n',\n );\n\n (async () => {\n try {\n await showCombinedLogs(logsBySlot, sources, level);\n // NOTE: do NOT unpause here; ESC will handle it.\n } catch {\n // If showCombinedLogs throws, recover and unpause\n viewing = false;\n setPaused(false);\n repaint();\n }\n })();\n };\n\n /**\n * Export combined logs (if an export manager was provided).\n *\n * @param which - Severity to export (e.g., \"error\", \"warn\", \"info\", \"all\").\n * @param label - Human-readable label for the export (e.g., \"error\", \"warn\").\n */\n const exportCombined = (\n which: 'error' | 'warn' | 'info' | 'all',\n label: string,\n ): void => {\n if (!exportMgr) return;\n try {\n const p = exportMgr.exportCombinedLogs(logsBySlot, which);\n say(`\\nWrote combined ${label} logs to: ${p}`);\n noteExport(which as keyof ExportStatusMap, p);\n } catch {\n say(`\\nFailed to write combined ${label} logs`);\n }\n };\n\n // The keypress handler the runner will attach to stdin.\n return (buf: Buffer): void => {\n const s = buf.toString('utf8');\n\n // Viewers (lowercase)\n if (s === 'e') {\n view(['err'], 'error');\n return;\n }\n if (s === 'w') {\n view(['warn', 'err'], 'warn');\n return;\n }\n if (s === 'i') {\n view(['info'], 'all');\n return;\n }\n if (s === 'l') {\n view(['out', 'err', 'structured'], 'all');\n return;\n }\n\n // Exports (uppercase) — enabled only when exportMgr is present\n if (s === 'E') {\n exportCombined('error', 'error');\n return;\n }\n if (s === 'W') {\n exportCombined('warn', 'warn');\n return;\n }\n if (s === 'I') {\n exportCombined('info', 'info');\n return;\n }\n if (s === 'A') {\n exportCombined('all', 'ALL');\n return;\n }\n\n // Command-specific bindings\n const fn = custom?.[s];\n if (fn) {\n fn({ noteExport, say });\n return;\n }\n\n // Exit a viewer (Esc / Ctrl+]) — resume dashboard\n if (s === '\\x1b' || s === '\\x1d') {\n viewing = false;\n setPaused(false);\n repaint();\n }\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const e=require(`./chunk-Bmb41Sf3.cjs`),t=require(`./constants-
|
|
1
|
+
const e=require(`./chunk-Bmb41Sf3.cjs`),t=require(`./constants-K6pQQtc7.cjs`),n=require(`./syncConfigurationToTranscend-DKliAJhK.cjs`),r=require(`./logger-DQwEYtSS.cjs`);let i=require(`@transcend-io/privacy-types`),a=require(`@transcend-io/type-utils`),o=require(`colors`);o=e.t(o);let s=require(`io-ts`);s=e.t(s);let c=require(`inquirer`);c=e.t(c);let l=require(`cli-progress`);l=e.t(l);let u=require(`@transcend-io/persisted-state`);const d=[`ENOTFOUND`,`ECONNRESET`,`ETIMEDOUT`,`502 Bad Gateway`,`504 Gateway Time-out`,`429`,`Rate limit exceeded`,`Task timed out after`,`unknown request error`].map(e=>e.toLowerCase());async function f(e,t,{maxAttempts:i=5,baseDelayMs:a=250,isRetryable:s=(e,t)=>d.some(e=>t.toLowerCase().includes(e)),onRetry:c}={}){let l=0;for(;;){l+=1;try{return await t()}catch(t){let u=n.O(t);if(!(l<i&&s(t,u)))throw Error(`${e} failed after ${l} attempt(s): ${u}`);c?.(l,t,u);let d=a*2**(l-1)+Math.floor(Math.random()*a);r.t.warn(o.default.yellow(`[retry] attempt ${l}/${i-1}; backing off ${d}ms: ${u}`)),await n.D(d)}}}const p=s.intersection([s.type({nodes:s.array(i.PreferenceQueryResponseItem)}),s.partial({cursor:s.string})]);async function m(e,{identifiers:t,partitionKey:i,skipLogging:s=!1,concurrency:c=40}){let u=[],d=n.Ns(t,100),m=new Date().getTime(),h=new l.default.SingleBar({},l.default.Presets.shades_classic);s||h.start(t.length,0);let g=0;await n.Ts(d,async t=>{let n=(0,a.decodeCodec)(p,await f(`Preference Query`,()=>e.post(`v1/preferences/${i}/query`,{json:{filter:{identifiers:t},limit:t.length}}).json(),{onRetry:(e,n,a)=>{r.t.warn(o.default.yellow(`[RETRY] group size=${t.length} partition=${i} attempt=${e}: ${a}`))}}));u.push(...n.nodes),g+=t.length,h.update(g)},{concurrency:c}),h.stop();let _=new Date().getTime()-m;return s||r.t.info(o.default.green(`Completed download in "${_/1e3}" seconds.`)),u}function h({row:e,columnToPurposeName:t,purposeSlugs:r,preferenceTopics:o}){let s={};return Object.entries(t).forEach(([t,{purpose:a,preference:c,valueMapping:l}])=>{if(!r.includes(a))throw Error(`Invalid purpose slug: ${a}, expected: ${r.join(`, `)}`);let u=e[t];if(c){let e=o.find(e=>e.slug===c&&e.purpose.trackingType===a);if(!e){let e=o.filter(e=>e.purpose.trackingType===a).map(e=>e.slug);throw Error(`Invalid preference slug: ${c} for purpose: ${a}. Allowed preference slugs for purpose are: ${e.join(`,`)}`)}switch(s[a]||(s[a]={preferences:[]}),s[a].preferences||(s[a].preferences=[]),e.type){case i.PreferenceTopicType.Boolean:{let e=l[u];if(e===void 0&&u!==``)throw Error(`No preference mapping found for value "${u}" in column "${t}" (purpose=${a}, preference=${c})`);if(e==null)return;if(typeof e!=`boolean`)throw Error(`Invalid value for boolean preference: ${c}, expected boolean, got: ${u}`);s[a].preferences.push({topic:c,choice:{booleanValue:e}});break}case i.PreferenceTopicType.Select:{let n=l[u];if(n===void 0&&u!==``)throw Error(`No preference mapping found for value "${u}" in column "${t}" (purpose=${a}, preference=${c})`);if(n==null)return;if(typeof n!=`string`)throw Error(`Invalid value for select preference: ${c}, expected string, got: ${u}`);let r=n.trim()||null;if(r&&!e.preferenceOptionValues.map(({slug:e})=>e).includes(r))throw Error(`Invalid value for select preference: ${c}, expected one of: ${e.preferenceOptionValues.map(({slug:e})=>e).join(`, `)}, got: ${u}`);s[a].preferences.push({topic:c,choice:{selectValue:r}});break}case i.PreferenceTopicType.MultiSelect:{if(typeof u!=`string`)throw Error(`Invalid value for multi select preference: ${c}, expected string, got: ${u}`);let r=n.li(u).map(n=>{let r=l[n];if(r===void 0&&u!==``)throw Error(`No preference mapping found for multi select token "${u}" in column "${t}" (purpose=${a}, preference=${c})`);if(r==null)return null;if(typeof r!=`string`)throw Error(`Invalid value for multi select preference: ${c}, expected one of: ${e.preferenceOptionValues.map(({slug:e})=>e).join(`, `)}, got: ${n}`);return r}).filter(e=>e!==null).sort((e,t)=>e.localeCompare(t));r.length>0&&s[a].preferences.push({topic:c,choice:{selectValues:r}});break}default:throw Error(`Unknown preference type: ${e.type}`)}}else{let n=l[u];if(n===void 0&&u!==``)throw Error(`No preference mapping found for value "${u}" in column "${t}" (purpose=${a}, preference=∅) ${JSON.stringify(e)}`);if(n===null)return;s[a]?s[a].enabled=n===!0:s[a]={enabled:n===!0}}}),(0,a.apply)(s,(e,t)=>{if(typeof e.enabled!=`boolean`)throw Error(`No mapping provided for purpose.enabled=true/false value: ${t}`);return{...e,enabled:e.enabled}})}const g=`[NONE]`;async function _(e,t){let i=n.js(n.Ds(e.map(e=>Object.keys(e)).flat()),[...t.identifierColumn?[t.identifierColumn]:[],...Object.keys(t.columnToPurposeName)]);if(!t.timestampColum){let{timestampName:e}=await c.default.prompt([{name:`timestampName`,message:`Choose the column that will be used as the timestamp of last preference update`,type:`list`,default:i.find(e=>e.toLowerCase().includes(`date`))||i.find(e=>e.toLowerCase().includes(`time`))||i[0],choices:[...i,g]}]);t.timestampColum=e}if(r.t.info(o.default.magenta(`Using timestamp column "${t.timestampColum}"`)),t.timestampColum!==g){let n=e.map((e,n)=>e[t.timestampColum]?null:[n]).filter(e=>!!e).flat();if(n.length>0)throw Error(`The timestamp column "${t.timestampColum}" is missing a value for the following rows: ${n.join(`
|
|
2
2
|
`)}`);r.t.info(o.default.magenta(`The timestamp column "${t.timestampColum}" is present for all row`))}return t}async function v(e,t){let i=n.js(n.Ds(e.map(e=>Object.keys(e)).flat()),[...t.identifierColumn?[t.identifierColumn]:[],...Object.keys(t.columnToPurposeName)]);if(!t.identifierColumn){let{identifierName:e}=await c.default.prompt([{name:`identifierName`,message:`Choose the column that will be used as the identifier to upload consent preferences by`,type:`list`,default:i.find(e=>e.toLowerCase().includes(`email`))||i[0],choices:i}]);t.identifierColumn=e}r.t.info(o.default.magenta(`Using identifier column "${t.identifierColumn}"`));let a=e.map((e,n)=>e[t.identifierColumn]?null:[n]).filter(e=>!!e).flat();if(a.length>0){let i=`The identifier column "${t.identifierColumn}" is missing a value for the following rows: ${a.join(`, `)}`;if(r.t.warn(o.default.yellow(i)),!await n.M({message:`Would you like to skip rows missing an identifier?`}))throw Error(i);let s=e.length;e=e.filter(e=>e[t.identifierColumn]),r.t.info(o.default.yellow(`Skipped ${s-e.length} rows missing an identifier`))}r.t.info(o.default.magenta(`The identifier column "${t.identifierColumn}" is present for all rows`));let s=n.As(e,t.identifierColumn),l=Object.entries(s).filter(([,e])=>e.length>1);if(l.length>0){let i=`The identifier column "${t.identifierColumn}" has duplicate values for the following rows: ${l.slice(0,10).map(([e,t])=>`${e} (${t.length})`).join(`
|
|
3
3
|
`)}`;if(r.t.warn(o.default.yellow(i)),!await n.M({message:`Would you like to automatically take the latest update?`}))throw Error(i);e=Object.entries(s).map(([,e])=>e.sort((e,n)=>new Date(n[t.timestampColum]).getTime()-new Date(e[t.timestampColum]).getTime())[0]).filter(e=>e)}return{currentState:t,preferences:e}}async function y(e,t,{purposeSlugs:a,preferenceTopics:s,forceTriggerWorkflows:l}){let u=n.js(n.Ds(e.map(e=>Object.keys(e)).flat()),[...t.identifierColumn?[t.identifierColumn]:[],...t.timestampColum?[t.timestampColum]:[]]);if(u.length===0){if(l)return t;throw Error(`No other columns to process`)}let d=[...a,...s.map(e=>`${e.purpose.trackingType}->${e.slug}`)];return await n.Es(u,async l=>{let u=n.Ds(e.map(e=>e[l])),f=t.columnToPurposeName[l];if(f)r.t.info(o.default.magenta(`Column "${l}" is associated with purpose "${f.purpose}"`));else{let{purposeName:e}=await c.default.prompt([{name:`purposeName`,message:`Choose the purpose that column ${l} is associated with`,type:`list`,default:d.find(e=>e.startsWith(a[0])),choices:d}]),[t,n]=e.split(`->`);f={purpose:t,preference:n||null,valueMapping:{}}}await n.Es(u,async e=>{if(f.valueMapping[e]!==void 0){r.t.info(o.default.magenta(`Value "${e}" is associated with purpose value "${f.valueMapping[e]}"`));return}if(f.preference===null){let{purposeValue:t}=await c.default.prompt([{name:`purposeValue`,message:`Choose the purpose value for value "${e}" associated with purpose "${f.purpose}"`,type:`confirm`,default:e!==`false`}]);f.valueMapping[e]=t}if(f.preference!==null){let t=s.find(e=>e.slug===f.preference);if(!t){r.t.error(o.default.red(`Preference topic "${f.preference}" not found`));return}let a=t.preferenceOptionValues.map(({slug:e})=>e);if(t.type===i.PreferenceTopicType.Boolean){let{preferenceValue:n}=await c.default.prompt([{name:`preferenceValue`,message:`Choose the preference value for "${t.slug}" value "${e}" associated with purpose "${f.purpose}"`,type:`confirm`,default:e!==`false`}]);f.valueMapping[e]=n;return}if(t.type===i.PreferenceTopicType.Select){let{preferenceValue:n}=await c.default.prompt([{name:`preferenceValue`,message:`Choose the preference value for "${t.slug}" value "${e}" associated with purpose "${f.purpose}"`,type:`list`,choices:a,default:a.find(t=>t===e)}]);f.valueMapping[e]=n;return}if(t.type===i.PreferenceTopicType.MultiSelect){await n.Es(n.li(e),async e=>{if(f.valueMapping[e]!==void 0)return;let{preferenceValue:n}=await c.default.prompt([{name:`preferenceValue`,message:`Choose the preference value for "${t.slug}" value "${e}" associated with purpose "${f.purpose}"`,type:`list`,choices:a,default:a.find(t=>t===e)}]);f.valueMapping[e]=n});return}throw Error(`Unknown preference topic type: ${t.type}`)}}),t.columnToPurposeName[l]=f}),t}function b({currentConsentRecord:e,pendingUpdates:t,preferenceTopics:n}){return Object.entries(t).every(([t,{preferences:r=[],enabled:a}])=>{let o=e.purposes.find(e=>e.purpose===t);return o&&o.enabled===a?r.every(({topic:e,choice:r})=>o.preferences&&o.preferences.find(a=>{if(a.topic!==e)return!1;let o=n.find(n=>n.slug===e&&n.purpose.trackingType===t);if(!o)throw Error(`Could not find preference topic for ${e}`);switch(o.type){case i.PreferenceTopicType.Boolean:return a.choice.booleanValue===r.booleanValue;case i.PreferenceTopicType.Select:return a.choice.selectValue===r.selectValue;case i.PreferenceTopicType.MultiSelect:let e=(a.choice.selectValues||[]).sort(),t=(r.selectValues||[]).sort();return e.length===t.length&&e.every((e,n)=>e===t[n]);default:throw Error(`Unknown preference topic type: ${o.type}`)}})):!1})}function x({currentConsentRecord:e,pendingUpdates:t,preferenceTopics:n,log:a}){return!!Object.entries(t).find(([t,{preferences:o=[],enabled:s}])=>{let c=e.purposes.find(e=>e.purpose===t);return c?c.enabled===s?!!o.find(({topic:o,choice:s})=>{let l=(c.preferences||[]).find(e=>e.topic===o);if(!l)return a&&r.t.warn(`No existing preference found for topic ${o} in purpose ${t} for user ${e.userId}.`),!1;let u=n.find(e=>e.slug===o&&e.purpose.trackingType===t);if(!u)throw Error(`Could not find preference topic for ${o}`);let d,f;switch(u.type){case i.PreferenceTopicType.Boolean:return d=l.choice.booleanValue!==s.booleanValue,a&&r.t.warn(`Preference topic ${o} boolean value conflict for user ${e.userId}. Expected: ${s.booleanValue}, Found: ${l.choice.booleanValue}`),d;case i.PreferenceTopicType.Select:return f=l.choice.selectValue!==s.selectValue,a&&r.t.warn(`Preference topic ${o} select value conflict for user ${e.userId}. Expected: ${s.selectValue}, Found: ${l.choice.selectValue}`),f;case i.PreferenceTopicType.MultiSelect:let t=(l.choice.selectValues||[]).sort(),n=(s.selectValues||[]).sort();return f=t.length!==n.length||!t.every((e,t)=>e===n[t]),a&&r.t.warn(`Preference topic ${o} multi-select value conflict for user ${e.userId}. Expected: ${n.join(`, `)}, Found: ${t.join(`, `)}`),f;default:throw Error(`Unknown preference topic type: ${u.type}`)}}):(a&&r.t.warn(`Purpose ${t} enabled value conflict for user ${e.userId}. Pending Value: ${s}, Current Value: ${c.enabled}`),!0):(a&&r.t.warn(`No existing purpose found for ${t} in consent record for ${e.userId}.`),!1)})}async function S({file:e,sombra:i,purposeSlugs:a,preferenceTopics:c,partitionKey:l,skipExistingRecordCheck:u,forceTriggerWorkflows:d},f){let p=new Date().getTime(),g=f.getValue(`fileMetadata`);r.t.info(o.default.magenta(`Reading in file: "${e}"`));let S=n.oi(e,s.record(s.string,s.string)),C={columnToPurposeName:{},pendingSafeUpdates:{},pendingConflictUpdates:{},skippedUpdates:{},...g[e]||{},lastFetchedAt:new Date().toISOString()};C=await _(S,C),g[e]=C,await f.setValue(g,`fileMetadata`);let w=await v(S,C);C=w.currentState,S=w.preferences,g[e]=C,await f.setValue(g,`fileMetadata`),C=await y(S,C,{preferenceTopics:c,purposeSlugs:a,forceTriggerWorkflows:d}),g[e]=C,await f.setValue(g,`fileMetadata`);let T=S.map(e=>e[C.identifierColumn]),E=t.g(u?[]:await m(i,{identifiers:T.map(e=>({value:e})),partitionKey:l}),`userId`);C.pendingConflictUpdates={},C.pendingSafeUpdates={},C.skippedUpdates={},S.forEach(e=>{let t=e[C.identifierColumn],n=h({row:e,columnToPurposeName:C.columnToPurposeName,preferenceTopics:c,purposeSlugs:a}),r=E[t];if(d&&!r)throw Error(`No existing consent record found for user with id: ${t}.
|
|
4
4
|
When 'forceTriggerWorkflows' is set all the user identifiers should contain a consent record`);if(r&&b({currentConsentRecord:r,pendingUpdates:n,preferenceTopics:c})&&!d){C.skippedUpdates[t]=e;return}if(r&&x({currentConsentRecord:r,pendingUpdates:n,preferenceTopics:c})){C.pendingConflictUpdates[t]={row:e,record:r};return}C.pendingSafeUpdates[t]=e}),g[e]=C,await f.setValue(g,`fileMetadata`);let D=new Date().getTime();r.t.info(o.default.green(`Successfully pre-processed file: "${e}" in ${(D-p)/1e3}s`))}const C=s.type({purpose:s.string,preference:s.union([s.string,s.null]),valueMapping:s.record(s.string,s.union([s.string,s.boolean,s.null,s.undefined]))}),w=s.record(s.string,C),T=s.type({name:s.string,isUniqueOnPreferenceStore:s.boolean}),E=s.record(s.string,T),D=s.type({key:s.string}),O=s.record(s.string,D),k=s.intersection([s.type({columnToPurposeName:s.record(s.string,C),lastFetchedAt:s.string,pendingSafeUpdates:s.record(s.string,s.record(s.string,s.string)),pendingConflictUpdates:s.record(s.string,s.type({record:i.PreferenceQueryResponseItem,row:s.record(s.string,s.string)})),skippedUpdates:s.record(s.string,s.record(s.string,s.string))}),s.partial({identifierColumn:s.string,timestampColum:s.string})]),A=s.record(s.string,s.union([s.boolean,i.PreferenceUpdateItem])),j=s.record(s.string,s.union([s.boolean,s.record(s.string,s.string)])),M=s.record(s.string,s.type({uploadedAt:s.string,error:s.string,update:i.PreferenceUpdateItem})),N=s.record(s.string,s.type({record:i.PreferenceQueryResponseItem,row:s.record(s.string,s.string)})),P=s.record(s.string,s.record(s.string,s.string)),F=s.type({fileMetadata:s.record(s.string,k),failingUpdates:s.record(s.string,s.type({uploadedAt:s.string,error:s.string,update:i.PreferenceUpdateItem})),pendingUpdates:s.record(s.string,i.PreferenceUpdateItem)}),I=s.type({records:s.array(s.type({anchorIdentifier:i.PreferenceStoreIdentifier,timestamp:s.string}))}),L=s.intersection([s.type({records:s.array(s.intersection([s.type({success:s.boolean}),s.partial({errorMessage:s.string})])),failures:s.array(s.type({index:s.number,error:s.string}))}),s.partial({errors:s.array(s.string)})]),R=s.type({name:s.string,value:s.string});async function z({auth:e,sombraAuth:t,receiptFilepath:i,file:s,partition:c,isSilent:d=!0,dryRun:f=!1,skipWorkflowTriggers:p=!1,skipConflictUpdates:m=!1,skipExistingRecordCheck:_=!1,attributes:v=[],transcendUrl:y,forceTriggerWorkflows:b=!1}){let x=n.ci(v),C=new u.PersistedState(i,F,{fileMetadata:{},failingUpdates:{},pendingUpdates:{}}),w=C.getValue(`failingUpdates`),T=C.getValue(`pendingUpdates`),E=C.getValue(`fileMetadata`);r.t.info(o.default.magenta(`Restored cache, there are:
|
|
5
5
|
${Object.values(w).length} failing requests to be retried\n${Object.values(T).length} pending requests to be processed\nThe following files are stored in cache and will be used:\n${Object.keys(E).map(e=>e).join(`
|
|
6
6
|
`)}\nThe following file will be processed: ${s}\n`));let D=n.ti(y,e),[O,k,A]=await Promise.all([n.ei(y,e,t),n.vr(D),n.Sr(D)]);await S({file:s,purposeSlugs:k.map(e=>e.trackingType),preferenceTopics:A,sombra:O,partitionKey:c,skipExistingRecordCheck:_,forceTriggerWorkflows:b},C);let j={};E=C.getValue(`fileMetadata`);let M=E[s];if(r.t.info(o.default.magenta(`Found ${Object.entries(M.pendingSafeUpdates).length} safe updates in ${s}`)),r.t.info(o.default.magenta(`Found ${Object.entries(M.pendingConflictUpdates).length} conflict updates in ${s}`)),r.t.info(o.default.magenta(`Found ${Object.entries(M.skippedUpdates).length} skipped updates in ${s}`)),Object.entries({...M.pendingSafeUpdates,...m?{}:(0,a.apply)(M.pendingConflictUpdates,({row:e})=>e)}).forEach(([e,t])=>{let n=M.timestampColum===g?new Date:new Date(t[M.timestampColum]),r=h({row:t,columnToPurposeName:M.columnToPurposeName,preferenceTopics:A,purposeSlugs:k.map(e=>e.trackingType)});j[e]={userId:e,partition:c,timestamp:n.toISOString(),purposes:Object.entries(r).map(([e,t])=>({...t,purpose:e,workflowSettings:{attributes:x,isSilent:d,skipWorkflowTrigger:p,...b?{forceTriggerWorkflow:b}:{}}}))}}),await C.setValue(j,`pendingUpdates`),await C.setValue({},`failingUpdates`),f){r.t.info(o.default.green(`Dry run complete, exiting. ${Object.values(j).length} pending updates. Check file: ${i}`));return}r.t.info(o.default.magenta(`Uploading ${Object.values(j).length} preferences to partition: ${c}`));let N=new Date().getTime(),P=new l.default.SingleBar({},l.default.Presets.shades_classic),I=0,L=Object.entries(j),R=n.Ns(L,p?100:10);P.start(L.length,0),await n.Ts(R,async e=>{try{await O.put(`v1/preferences`,{json:{records:e.map(([,e])=>e),skipWorkflowTriggers:p}}).json()}catch(t){try{let e=JSON.parse(t?.response?.body||`{}`);e.error&&r.t.error(o.default.red(`Error: ${e.error}`))}catch{}r.t.error(o.default.red(`Failed to upload ${e.length} user preferences to partition ${c}: ${t?.response?.body||t?.message}`));let n=C.getValue(`failingUpdates`);e.forEach(([e,r])=>{n[e]={uploadedAt:new Date().toISOString(),update:r,error:t?.response?.body||t?.message||`Unknown error`}}),await C.setValue(n,`failingUpdates`)}I+=e.length,P.update(I)},{concurrency:40}),P.stop();let z=new Date().getTime()-N;r.t.info(o.default.green(`Successfully uploaded ${L.length} user preferences to partition ${c} in "${z/1e3}" seconds!`))}function B({identifiers:e=[],purposes:t=[],metadata:n=[],consentManagement:r={},system:i={decryptionStatus:`DECRYPTED`},...a},o){let s={...a,...i,...r};if(Array.isArray(e)){let t=new Map;for(let{name:n,value:r}of e)t.has(n)||t.set(n,new Set),r&&t.get(n).add(r);for(let[e,n]of t.entries())s[e]=Array.from(n).join(o)}if(Array.isArray(n)&&(s.metadata=JSON.stringify(n.reduce((e,{key:t,value:n})=>(e[t]=n,e),{}))),Array.isArray(t)){for(let{purpose:e,preferences:n,enabled:r}of t)if(s[e]=!!r,Array.isArray(n))for(let{topic:t,choice:r}of n){let n=`${e}_${t}`,i=null;i=typeof r.booleanValue==`boolean`?r.booleanValue:r.selectValue?r.selectValue:Array.isArray(r.selectValues)?r.selectValues.filter(e=>e.length>0).join(`,`):null,s[n]=i}}return s}async function*V(e,t,n,i){let s;for(;;){let c={limit:i};n&&Object.keys(n).length&&(c.filter=n),s&&(c.cursor=s);let{nodes:l,cursor:u}=(0,a.decodeCodec)(p,await f(`Preference Query`,()=>e.post(`v1/preferences/${t}/query`,{json:c}).json(),{onRetry:(e,t,n)=>{r.t.warn(o.default.yellow(`Retry attempt ${e} for iterateConsentPages due to error: ${n}`))}}));if(!l?.length||(yield l,!u))break;s=u}}function H(e){return e.timestampAfter||e.timestampBefore?`timestamp`:`updated`}function U(e,t){return e===`timestamp`?new Date(t.timestamp):t.system?.updatedAt?new Date(t.system.updatedAt):new Date}function W(e,t){if(e===`timestamp`)return{after:t.timestampAfter?new Date(t.timestampAfter):void 0,before:t.timestampBefore?new Date(t.timestampBefore):void 0};let n=t.system??{};return{after:n.updatedAfter?new Date(n.updatedAfter):void 0,before:n.updatedBefore?new Date(n.updatedBefore):void 0}}function G(e,t,n){return e===`timestamp`?{...t,timestampBefore:n??t.timestampBefore}:{...t,system:{...t.system||{},...n?{updatedBefore:n}:{}},timestampAfter:void 0,timestampBefore:void 0}}async function K(e,t,n){r.t.info(o.default.magenta(`Single-record probe with filter: ${JSON.stringify(n)}`));let i=await V(e,t,n,1).next();if(i.done||!i.value||i.value.length===0)return r.t.info(o.default.yellow(`Probe result: no record`)),null;let a=i.value[0];return r.t.info(o.default.green(`Probe result: found record at ${U(H(n),a).toISOString()}`)),a}async function q(e,t){let{partition:i,mode:a,baseFilter:s,maxLookbackDays:c=3650}=t,l=await K(e,i,G(a,s));if(!l)return r.t.info(o.default.yellow(`No records found; defaulting earliest day to today.`)),n.y(new Date);let u=U(a,l);r.t.info(o.default.green(`Newest instant: ${u.toISOString()}`));let d=[1,7,30],f=0,p=d[0]*n.f,m=u,h=null;for(;;){let t=f<d.length?new Date(u.getTime()-d[f]*n.f):new Date(u.getTime()-p);if((n.y(new Date).getTime()-n.y(t).getTime())/n.f>c){r.t.warn(o.default.yellow(`Exponential jump exceeded maxLookbackDays=${c}. Using current bounds.`)),h=t;break}r.t.info(o.default.magenta(`Probing before=${t.toISOString()} (jump step ${f<d.length?`${d[f]}d`:`${Math.round(p/n.f)}d`})…`));let l=await K(e,i,G(a,s,t.toISOString()));if(l){m=U(a,l),r.t.info(o.default.green(`Found older record at ${m.toISOString()} — continue jumping back.`)),f<d.length-1?(f+=1,p=d[f]*n.f):f===d.length-1?(f+=1,p=d[d.length-1]*2*n.f):p*=2;continue}h=t,r.t.info(o.default.green(`No record before ${t.toISOString()} — established empty lower bound.`));break}h||=new Date(m.getTime()-n.f);let g=h,_=m,v=Math.max(n.f,Math.floor((_.getTime()-g.getTime())/64));r.t.info(o.default.magenta(`Exponential forward-from-empty start: empty=${g.toISOString()} found=${_.toISOString()} step=${Math.round(v/n.f)}d`));for(let t=0;t<8;t+=1){let t=new Date(g.getTime()+v);if(t.getTime()>=_.getTime())break;r.t.info(o.default.magenta(`Forward gallop probe before=${t.toISOString()}…`));let c=await K(e,i,G(a,s,t.toISOString()));if(c?(_=U(a,c),r.t.info(o.default.green(`Gallop hit at ${_.toISOString()} — tightening found bound. Next step halves.`)),v=Math.max(n.f,Math.floor(v/2))):(g.setTime(t.getTime()),r.t.info(o.default.yellow(`Gallop miss — advancing empty bound to ${g.toISOString()}. Next step doubles.`)),v=Math.min(_.getTime()-g.getTime(),v*2),v<n.f&&(v=n.f)),_.getTime()-g.getTime()<=n.f)break}for(;_.getTime()-g.getTime()>n.f;){let t=new Date(g.getTime()+Math.floor((_.getTime()-g.getTime())/2));r.t.info(o.default.magenta(`Binary probe before=${t.toISOString()}…`));let n=await K(e,i,G(a,s,t.toISOString()));if(n){let e=U(a,n);r.t.info(o.default.green(`Binary probe found record at ${e.toISOString()}.`)),_=e}else r.t.info(o.default.yellow(`Binary probe found no record.`)),g=t}let y=n.y(_);return r.t.info(o.default.green(`Earliest day (UTC) resolved to ${y.toISOString()} (instant ≈ ${_.toISOString()}).`)),y}async function J(e,t){let{partition:i,mode:a,baseFilter:s}=t;r.t.info(o.default.magenta(`Latest-day discovery: probing newest record…`));let c=await K(e,i,G(a,s));if(!c)return r.t.info(o.default.yellow(`No records found at all; defaulting latest day to today.`)),n.y(new Date);let l=U(a,c);r.t.info(o.default.green(`Newest record instant is ${l.toISOString()}.`));let u=n.y(l);return r.t.info(o.default.green(`Latest day (UTC) resolved to ${u.toISOString()} from instant ${l.toISOString()}.`)),u}function Y(e,t,r,i=5e3){let a=Math.max(0,r.getTime()-t.getTime());if(a===0)return[];let o=new Date(Math.floor(t.getTime()/n.p)*n.p),s=Math.ceil(a/Math.max(1,i)),c=Math.max(n.p,s),l=Math.ceil((r.getTime()-o.getTime())/c),u=[];for(let t=0;t<l;t+=1){let n=o.getTime()+t*c,i=Math.min(r.getTime(),n+c)-1,a=Math.max(n,i),s=new Date(n).toISOString(),l=new Date(a).toISOString();e===`timestamp`?u.push({timestampAfter:s,timestampBefore:l}):u.push({system:{updatedAfter:s,updatedBefore:l}})}return u}function X(e,t,n){return e===`timestamp`?{...t,timestampAfter:n.timestampAfter??t.timestampAfter,timestampBefore:n.timestampBefore??t.timestampBefore,system:void 0}:{...t,system:{...t.system||{},...n.system?.updatedAfter?{updatedAfter:n.system.updatedAfter}:{},...n.system?.updatedBefore?{updatedBefore:n.system.updatedBefore}:{}},timestampAfter:void 0,timestampBefore:void 0}}async function Z(e,{partition:t,filterBy:i={},limit:a=50,windowConcurrency:s=25,maxChunks:c=5e3,maxLookbackDays:u=3650,onItems:d}){let f=H(i);r.t.info(o.default.magenta(`Fetching consent preferences in chunks by ${f===`timestamp`?`timestamp`:`system.updatedAt`}...`));let{after:p,before:m}=W(f,i);if(r.t.info(o.default.magenta(`Initial bounds: after=${p?.toISOString()??`undefined`} before=${m?.toISOString()??`undefined`}`)),(!p||!m)&&(p||(r.t.info(o.default.magenta(`Discovering earliest day with data for partition ${t}...`)),p=await q(e,{partition:t,mode:f,baseFilter:i,maxLookbackDays:u}),r.t.info(o.default.green(`Discovered earliest day with data: ${p.toISOString()}`))),!m)){r.t.info(o.default.magenta(`Discovering latest day with data for partition ${t}...`));let a=await J(e,{partition:t,mode:f,baseFilter:i,earliest:p});m=n.h(a,1),r.t.info(o.default.green(`Discovered latest day with data: ${a.toISOString()}`))}r.t.info(o.default.green(`Final bounds (UTC): after=${p.toISOString()} before=${m.toISOString()}`));let h=Y(f,p,m,c);r.t.info(o.default.magenta(`Fetching consent preferences from partition ${t} in ${h.length} chunks...`));let g=new l.default.SingleBar({format:`Downloading [{bar}] {percentage}% | chunks {value}/{total} | fetched {fetched}`},l.default.Presets.shades_classic),_=0,v=0;g.start(h.length,0,{fetched:v});let y=Date.now(),b=n._(a),x=[];return await n.Ts(h.map((e,t)=>({windowFilter:e,idx:t})),async({windowFilter:n})=>{let r=X(f,i,n);for await(let n of V(e,t,r,b))v+=n.length,g.update(_,{fetched:v}),d?await d(n):x.push(...n);_+=1,g.update(_,{fetched:v})},{concurrency:Math.max(1,s)}),g.update(_,{fetched:v}),g.stop(),r.t.info(o.default.green(`Fetched ${v} consent preference records from partition ${t} in ${(Date.now()-y)/1e3}s.`)),d?[]:x}async function Q(e,{partition:t,filterBy:n={},limit:i=50,onItems:s}){let c=[],l,u=n&&(Object.keys(n).length>0||n.system&&Object.keys(n.system).length>0),d=Math.max(1,Math.min(50,i??50));for(;;){let i={limit:d};u&&(i.filter=n),l&&(i.cursor=l);let{nodes:m,cursor:h}=(0,a.decodeCodec)(p,await f(`Preference Query`,()=>e.post(`v1/preferences/${t}/query`,{json:i}).json(),{onRetry:(e,t,n)=>{r.t.warn(o.default.yellow(`Retry attempt ${e} for fetchConsentPreferences due to error: ${n}`))}}));if(!m||m.length===0||(s?await s(m):c.push(...m),!h))break;l=h}return s?[]:c}async function $(e,{partition:t,identifierChunk:n,timestamp:i}){try{let{failures:s}=(0,a.decodeCodec)(L,await f(`Delete Preference Records`,()=>e.post(`v1/preferences/${t}/delete`,{json:{records:n.map(e=>({anchorIdentifier:e,timestamp:i.toISOString()}))}}).json(),{maxAttempts:3,onRetry:(e,t,n)=>{r.t.warn(o.default.yellow(`Attempt ${e} to delete preference records failed: ${n}`))}}));return s.length>0?s.map(({index:e,error:t})=>({...n[e],error:t})):[]}catch(e){return n.map(t=>({...t,error:e.message}))}}async function ee(e,{partition:t,filePath:r,timestamp:i,maxItemsInChunk:a,maxConcurrency:o}){return(await n.Ts(n.Ns(n.oi(r,R),a),async n=>await $(e,{partition:t,identifierChunk:n,timestamp:i}),{concurrency:o})).flat()}Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return z}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return B}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return Q}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return Z}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return ee}});
|
|
7
|
-
//# sourceMappingURL=preference-management-
|
|
7
|
+
//# sourceMappingURL=preference-management-B36PQuMK.cjs.map
|